How do I (elegantly) transpose textbox over label at specific part of string?

前端 未结 10 404
谎友^
谎友^ 2020-12-16 11:54

I\'ll be feeding a number of strings into labels on a Windows Form (I don\'t use these a lot). The strings will be similar to the following:

\"The qui

相关标签:
10条回答
  • 2020-12-16 12:37

    Another quiz solution, using a class derived from TextBox a the editor for the missing letters.

    What this code does:
    1) Takes the Text of a Label control, a list of strings (words) and substrings of those words which are used as a mask to hide some of the letters in the words
    2) Creates a mask of the substrings using two Unicode space chars (U+2007 and U+2002) of different sizes, to match the size of the letters to substitute
    3) Sizes up a border-less TextBox (the Editor, a class object that inherits from Textbox) using the calculated Width and Height (in pixels) of the substring. Sets the TextBox.MaxLength property to the length of the substring.
    4) Calculates the position of the substrings inside a Multiline Label Text, checks for duplicate patterns, and overlays the Texbox objects (Editor class)

    This method supports:
    Proportional fonts. Only Unicode Fonts are supported.
    The Labels' text can occupy multiple lines.

    I've use a fixed-size font (Lucida Console) because of the mask char.
    To deal with proportional fonts, two different mask chars are used, depending on the characters width
    (i.e. different mask chars of different width to match the substituted characters width).

    A visual representation of the results:
    The TAB key is used to pass from a TextBox control to the next/previous.
    The ENTER key is used to accept the edit. Then the code checks if it's a match.
    The ESC key resets the text and shows the initial mask.

    A list of words is initialized specifying a complete word and a number of contiguous characters to substitute with a mask: => jumped : umpe
    and the associated Label control.
    When a Quiz class is initialized, it automatically subtitutes all words in the specified Label text with a TextBox mask.

    public class QuizWord
    {
        public string Word { get; set; }
        public string WordMask { get; set; }
    }
    
    List<Quiz> QuizList = new List<Quiz>();
    
    QuizList.Add(new Quiz(lblSampleText1,
                 new List<QuizWord>  
               { new QuizWord { Word = "jumped", WordMask = "umpe" }, 
                 new QuizWord { Word = "lazy", WordMask = "az" } }));
    QuizList.Add(new Quiz(lblSampleText2,
                 new List<QuizWord>  
               { new QuizWord { Word = "dolor", WordMask = "olo" }, 
                 new QuizWord { Word = "elit", WordMask = "li" } }));
    QuizList.Add(new Quiz(lblSampleText3,
                 new List<QuizWord>  
               { new QuizWord { Word = "Brown", WordMask = "row" }, 
                 new QuizWord { Word = "Foxes", WordMask = "oxe" }, 
                 new QuizWord { Word = "latinorum", WordMask = "atinoru" },
                 new QuizWord { Word = "Support", WordMask = "uppor" } }));
    

    This is the Quiz class:
    It's job is to collect all the Editors (TextBoxes) that are used for each Label and calculate their Location, given the position of the string they have to substitute in each Label text.

    public class Quiz : IDisposable
    {
        private bool _disposed = false;
        private List<QuizWord> _Words = new List<QuizWord>();
        private List<Editor> _Editors = new List<Editor>();
        private MultilineSupport _Multiline;
        private Control _Container = null;
    
        public Quiz() : this(null, null) { }
        public Quiz(Label RefControl, List<QuizWord> Words)
        {
            this._Container = RefControl.Parent;
    
            this.Label = null;
            if (RefControl != null)
            {
                this.Label = RefControl;
                this.Matches = new List<QuizWord>();
                if (Words != null)
                {
                    this._Multiline = new MultilineSupport(RefControl);
                    this.Matches = Words;
                }
            }
        }
    
        public Label Label { get; set; }
        public List<QuizWord> Matches
        {
            get { return this._Words; }
            set { this._Words = value; Editors_Setup(); }
        }
    
        private void Editors_Setup()
        {
            if ((this._Words == null) || (this._Words.Count < 1)) return;
            int i = 1;
            foreach (QuizWord _word in _Words)
            {
                List<Point> _Positions = GetEditorsPosition(this.Label.Text, _word);
                foreach (Point _P in _Positions)
                {
                    Editor _editor = new Editor(this.Label, _word.WordMask);
                    _editor.Location = _P;
                    _editor.Name = this.Label.Name + "Editor" + i.ToString(); ++i;
                    _Editors.Add(_editor);
                    this._Container.Controls.Add(_editor);
                    this._Container.Controls[_editor.Name].BringToFront();
                }
            }
        }
    
        private List<Point> GetEditorsPosition(string _labeltext, QuizWord _word)
        {
            return  Regex.Matches(_labeltext, _word.WordMask) 
                         .Cast<Match>()
                         .Select(t => t.Index).ToList()
                         .Select(idx => this._Multiline.GetPositionFromCharIndex(idx))
                         .ToList();
        }
    
        private class MultilineSupport
        {
            Label RefLabel;
            float _FontSpacingCoef = 1.8F;
            private TextFormatFlags _flags = TextFormatFlags.SingleLine | TextFormatFlags.Left |
                                             TextFormatFlags.NoPadding | TextFormatFlags.TextBoxControl;
    
            public MultilineSupport(Label label)
            {
                this.Lines = new List<string>();
                this.LinesFirstCharIndex = new List<int>();
                this.NumberOfLines = 0;
                Initialize(label);
            }
    
            public int NumberOfLines { get; set; }
            public List<string> Lines { get; set; }
            public List<int> LinesFirstCharIndex { get; set; }
    
            public int GetFirstCharIndexFromLine(int line)
            {
                if (LinesFirstCharIndex.Count == 0) return -1;
                return LinesFirstCharIndex.Count - 1 >= line ? LinesFirstCharIndex[line] : -1;
            }
    
            public int GetLineFromCharIndex(int index)
            {
                if (LinesFirstCharIndex.Count == 0) return -1;
                return LinesFirstCharIndex.FindLastIndex(idx => idx <= Index);;
            }
    
            public Point GetPositionFromCharIndex(int Index)
            {
                return CalcPosition(GetLineFromCharIndex(Index), Index);
            }
    
            private void Initialize(Label label)
            {
                this.RefLabel = label;
                if (label.Text.Trim().Length == 0)
                    return;
    
                List<string> _wordslist = new List<string>();
                string _substring = string.Empty;
                this.LinesFirstCharIndex.Add(0);
                this.NumberOfLines = 1;
                int _currentlistindex = 0;
                int _start = 0;
    
                _wordslist.AddRange(label.Text.Split(new char[] { (char)32 }, StringSplitOptions.None));
                foreach (string _word in _wordslist)
                {
                    ++_currentlistindex;
                    int _wordindex = label.Text.IndexOf(_word, _start);
                    int _sublength = MeasureString((_substring + _word + (_currentlistindex < _wordslist.Count ? ((char)32).ToString() : string.Empty)));
                    if (_sublength > label.Width)
                    {
                        this.Lines.Add(_substring);
                        this.LinesFirstCharIndex.Add(_wordindex);
                        this.NumberOfLines += 1;
                        _substring = string.Empty;
                    }
                    _start += _word.Length + 1;
                    _substring += _word + (char)32;
                }
                this.Lines.Add(_substring.TrimEnd());
            }
    
            private Point CalcPosition(int Line, int Index)
            {
                int _font_padding = (int)((RefLabel.Font.Size - (int)(RefLabel.Font.Size % 12)) * _FontSpacingCoef);
                int _verticalpos = Line * this.RefLabel.Font.Height + this.RefLabel.Top;
                int _horizontalpos = MeasureString(this.Lines[Line].Substring(0, Index - GetFirstCharIndexFromLine(Line)));
                return new Point(_horizontalpos + _font_padding, _verticalpos);
            }
    
            private int MeasureString(string _string)
            {
                return TextRenderer.MeasureText(RefLabel.CreateGraphics(), _string,
                                                this.RefLabel.Font, this.RefLabel.Size, _flags).Width;
            }
        }
    
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    
        protected void Dispose(bool IsSafeDisposing)
        {
            if (IsSafeDisposing && (!this._disposed))
            {
                foreach (Editor _editor in _Editors)
                    if (_editor != null) _editor.Dispose();
                this._disposed = true;
            }
        }
    }
    

    This is the Editor class (inherits from TextBox):
    It builds and calculates the length of the mask chars and autosizes itself using this value.
    Has basic editing capabilities.

    public class Editor : TextBox
    {
        private string SubstChar = string.Empty;
        private string SubstCharLarge = ((char)0x2007).ToString();
        private string SubstCharSmall = ((char)0x2002).ToString();
        private Font NormalFont = null;
        private Font UnderlineFont = null;
        private string WordMask = string.Empty;
        private TextFormatFlags _flags = TextFormatFlags.NoPadding | TextFormatFlags.Left |
                                         TextFormatFlags.Bottom | TextFormatFlags.WordBreak |
                                         TextFormatFlags.TextBoxControl;
    
        public Editor(Label RefLabel, string WordToMatch)
        {
            this.BorderStyle = BorderStyle.None;
            this.TextAlign = HorizontalAlignment.Left;
            this.Margin = new Padding(0);
    
            this.MatchWord = WordToMatch;
            this.MaxLength = WordToMatch.Length;
            this._Label = RefLabel;
            this.NormalFont = RefLabel.Font;
            this.UnderlineFont = new Font(RefLabel.Font, (RefLabel.Font.Style | FontStyle.Underline));
            this.Font = this.UnderlineFont;
            this.Size = GetTextSize(WordToMatch);
            this.WordMask = CreateMask(this.Size.Width);
            this.Text = this.WordMask;
            this.BackColor = RefLabel.BackColor;
            this.ForeColor = RefLabel.ForeColor;
    
            this.KeyDown += this.KeyDownHandler;
            this.Enter += (sender, e) => { this.Font = this.UnderlineFont; this.SelectionStart = 0;  this.SelectionLength = 0; };
            this.Leave += (sender, e) => { CheckWordMatch(); };
        }
    
        public string MatchWord { get; set; }
        private Label _Label { get; set; }
    
        public void KeyDownHandler(object sender, KeyEventArgs e)
        {
            int _start = this.SelectionStart;
            switch (e.KeyCode)
            {
            case Keys.Back:
                if (this.SelectionStart > 0)
                {
                    this.AppendText(SubstChar);
                    this.SelectionStart = 0;
                    this.ScrollToCaret();
                }
                this.SelectionStart = _start;
                break;
            case Keys.Delete:
                if (this.SelectionStart < this.Text.Length)
                {
                    this.AppendText(SubstChar);
                    this.SelectionStart = 0;
                    this.ScrollToCaret();
                }
                this.SelectionStart = _start;
                break;
            case Keys.Enter:
                e.SuppressKeyPress = true;
                CheckWordMatch();
                break;
            case Keys.Escape:
                e.SuppressKeyPress = true;
                this.Text = this.WordMask;
                this.ForeColor = this._Label.ForeColor;
                break;
            default:
                if ((e.KeyCode >= (Keys)32 & e.KeyCode <= (Keys)127) && (e.KeyCode < (Keys)36 | e.KeyCode > (Keys)39))
                {
                    int _removeat = this.Text.LastIndexOf(SubstChar);
                    if (_removeat > -1) this.Text = this.Text.Remove(_removeat, 1);
                    this.SelectionStart = _start;
                }
                break;
            }
        }
        private void CheckWordMatch()
        {
            if (this.Text != this.WordMask) {
                this.Font = this.Text == this.MatchWord ? this.NormalFont : this.UnderlineFont;
                this.ForeColor = this.Text == this.MatchWord ? Color.Green : Color.Red;
            } else {
                this.ForeColor = this._Label.ForeColor;
            }
        }
    
        private Size GetTextSize(string _mask)
        {
            return TextRenderer.MeasureText(this._Label.CreateGraphics(), _mask, this._Label.Font, this._Label.Size, _flags);
        }
    
        private string CreateMask(int _EditorWidth)
        {
            string _TestMask = new StringBuilder().Insert(0, SubstCharLarge, this.MatchWord.Length).ToString();
            SubstChar = (GetTextSize(_TestMask).Width <= _EditorWidth) ? SubstCharLarge : SubstCharSmall;
            return SubstChar == SubstCharLarge 
                              ? _TestMask  
                              : new StringBuilder().Insert(0, SubstChar, this.MatchWord.Length).ToString();
        }
    }
    
    0 讨论(0)
  • 2020-12-16 12:39

    Work out which character was clicked on, if it was an underscore then size up the underscores left and right and show a textbox on top of the underscores.

    You can tweak this code, the label is actually a Read-Only textbox to get acces to the GetCharIndexFromPosition and GetPositionFromCharIndex methods.

    namespace WindowsFormsApp1
    {
        public partial class Form1 : Form
        {
            private System.Windows.Forms.TextBox txtGap;
            private System.Windows.Forms.Label label2;
            private System.Windows.Forms.Label lblClickedOn;
            private System.Windows.Forms.TextBox txtTarget;
    
            private void txtTarget_MouseDown(object sender, MouseEventArgs e)
            {
                int index = txtTarget.GetCharIndexFromPosition(e.Location);
                //Debugging help
                Point pt = txtTarget.GetPositionFromCharIndex(index);
                lblClickedOn.Text = index.ToString();
    
                txtGap.Visible = false;
    
                if (txtTarget.Text[index] == (char)'_')
                {
                    //Work out the left co-ordinate for the textbox by checking the number of underscores prior
                    int priorLetterToUnderscore = 0;
                    for (int i = index - 1; i > -1; i--)
                    {
                        if (txtTarget.Text[i] != (char)'_')
                        {
                            priorLetterToUnderscore = i + 1;
                            break;
                        }
                    }
    
                    int afterLetterToUnderscore = 0;
                    for (int i = index + 1; i <= txtTarget.Text.Length; i++)
                    {
                        if (txtTarget.Text[i] != (char)'_')
                        {
                            afterLetterToUnderscore = i;
                            break;
                        }
                    }
    
    
                    //Measure the characters width earlier than the priorLetterToUnderscore
                    pt = txtTarget.GetPositionFromCharIndex(priorLetterToUnderscore);
                    int left = pt.X + txtTarget.Left;
    
                    pt = txtTarget.GetPositionFromCharIndex(afterLetterToUnderscore);
                    int width = pt.X + txtTarget.Left - left;
    
                    //Check the row/line we are on
                    SizeF textSize = this.txtTarget.CreateGraphics().MeasureString("A", this.txtTarget.Font, this.txtTarget.Width);
                    int line = pt.Y / (int)textSize.Height;
    
                    txtGap.Location = new Point(left, txtTarget.Top + (line * (int)textSize.Height));
                    txtGap.Width = width;
                    txtGap.Text = string.Empty;
                    txtGap.Visible = true;
    
                 }
            }
    
            private void Form1_Click(object sender, EventArgs e)
            {
                txtGap.Visible = false;
            }
    
            public Form1()
            {
                this.txtGap = new System.Windows.Forms.TextBox();
                this.label2 = new System.Windows.Forms.Label();
                this.lblClickedOn = new System.Windows.Forms.Label();
                this.txtTarget = new System.Windows.Forms.TextBox();
                this.SuspendLayout();
                // 
                // txtGap
                // 
                this.txtGap.Font = new System.Drawing.Font("Microsoft Sans Serif", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.txtGap.Location = new System.Drawing.Point(206, 43);
                this.txtGap.Name = "txtGap";
                this.txtGap.Size = new System.Drawing.Size(25, 20);
                this.txtGap.TabIndex = 1;
                this.txtGap.Text = "ump";
                this.txtGap.Visible = false;
                // 
                // label2
                // 
                this.label2.AutoSize = true;
                this.label2.Location = new System.Drawing.Point(22, 52);
                this.label2.Name = "label2";
                this.label2.Size = new System.Drawing.Size(84, 13);
                this.label2.TabIndex = 2;
                this.label2.Text = "Char clicked on:";
                // 
                // lblClickedOn
                // 
                this.lblClickedOn.AutoSize = true;
                this.lblClickedOn.Location = new System.Drawing.Point(113, 52);
                this.lblClickedOn.Name = "lblClickedOn";
                this.lblClickedOn.Size = new System.Drawing.Size(13, 13);
                this.lblClickedOn.TabIndex = 3;
                this.lblClickedOn.Text = "_";
                // 
                // txtTarget
                // 
                this.txtTarget.BackColor = System.Drawing.SystemColors.Menu;
                this.txtTarget.BorderStyle = System.Windows.Forms.BorderStyle.None;
                this.txtTarget.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.txtTarget.Location = new System.Drawing.Point(22, 21);
                this.txtTarget.Name = "txtTarget";
                this.txtTarget.ReadOnly = true;
                this.txtTarget.Size = new System.Drawing.Size(317, 16);
                this.txtTarget.TabIndex = 4;
                this.txtTarget.Text = "The quick brown fox j___ed over the l__y hound";
                this.txtTarget.MouseDown += new System.Windows.Forms.MouseEventHandler(this.txtTarget_MouseDown);
                // 
                // Form1
                // 
                this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
                this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
                this.ClientSize = new System.Drawing.Size(394, 95);
                this.Controls.Add(this.txtGap);
                this.Controls.Add(this.txtTarget);
                this.Controls.Add(this.lblClickedOn);
                this.Controls.Add(this.label2);
                this.Name = "Form1";
                this.Text = "Form1";
                this.Click += new System.EventHandler(this.Form1_Click);
                this.ResumeLayout(false);
                this.PerformLayout();
            }
        }        
    }
    

    To disable the Textbox (fake label) from being selected: https://stackoverflow.com/a/42391380/495455

    Edit:

    I made it work for multiline textboxes:

    0 讨论(0)
  • 2020-12-16 12:39

    I would try something like this (sure will need some sizes adjustments):

        var indexOfCompletionString = label.Text.IndexOf("____");
        var labelLeftPos = label.Left;
        var labelTopPos =  label.Top;
    
        var completionStringMeasurments = this.CreateGraphics().MeasureString("____", label.Font);
        var substr = label.Text.Substring(0, indexOfCompletionString);
        var substrMeasurments =  this.CreateGraphics().MeasureString(substr, label.Font);
    
        var tBox = new TextBox
        {
            Height = (int)completionStringMeasurments.Height,
            Width = (int)completionStringMeasurments.Width,
            Location = new Point(labelLeftPos + (int)substrMeasurments.Width, labelTopPos)
        };
    
        tBox.BringToFront();
        Controls.Add(tBox);
        Controls.SetChildIndex(tBox, 0);
    
    0 讨论(0)
  • 2020-12-16 12:42

    To satisfy this requirement, IMO it's better to use those features of Windows Forms which allow interoperability with HTML or WPF and Host a WebBrowser control or a WPF ElementHost to show the content to users. Before reading this answer, please consider:

    • Users should not be able to clear the ____ fields. If they can clear them, once they moved to another blank, they will lose the ability to find the cleared field.
    • It's better to allow users to use Tab key to move between ____ fields.
    • As it's mentioned in the question: A MaskTextBox won't work as I need multiline support.
    • As it's mentioned in the question: There'll be 300+ strings so mixing a lot of Windows Forms control is not a good idea.

    Using Html as View of a C# model and show it in WebBrowser control

    Here I will share a simple answer based on showing HTML in WebBrowser control. As an option you can use a WebBrowser control and create suitable html to show in WebBrowser control using a mode class.

    The main idea is creating an html output based on the quiz model (including the original text and ragnes of blanks) and rendering the model using html and showing it in a WebBrowser control.

    For example using following model:

    quiz = new Quiz();
    quiz.Text = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
    quiz.Ranges.Add(new SelectionRange(6, 5));
    quiz.Ranges.Add(new SelectionRange(30, 7));
    quiz.Ranges.Add(new SelectionRange(61, 2));
    quiz.Ranges.Add(new SelectionRange(82, 6));
    

    It will render this output:

    fill in the blank - initial

    Then after the user entered values, it will show this way:

    fill in the blank - having answers

    And at last, when you click on Show Result button, it will show the correct answers in green color, and wrong answers in red color:

    fill in the blank - showing results

    Code

    You can download full working source code for example here:

    • r-aghaei/FillInTheBlankQuizSample

    The implementation is quiet simple:

    public class Quiz
    {
        public Quiz() { Ranges = new List<SelectionRange>(); }
        public string Text { get; set; }
        public List<SelectionRange> Ranges { get; private set; }
        public string Render()
        {
            /* rendering logic*/
        }
    }
    

    Here is the complete code of the Quiz class:

    public class Quiz
    {
        public Quiz() { Ranges = new List<SelectionRange>(); }
        public string Text { get; set; }
        public List<SelectionRange> Ranges { get; private set; }
        public string Render()
        {
            var content = new StringBuilder(Text);
            for (int i = Ranges.Count - 1; i >= 0; i--)
            {
                content.Remove(Ranges[i].Start, Ranges[i].Length);
                var length = Ranges[i].Length;
                var replacement = $@"<input id=""q{i}"" 
                    type=""text"" class=""editable""
                    maxlength=""{length}"" 
                    style=""width: {length*1.162}ch;"" />";
                content.Insert(Ranges[i].Start, replacement);
            }
            var result = string.Format(Properties.Resources.Template, content);
            return result;
        }
    }
    
    public class SelectionRange
    {
        public SelectionRange(int start, int length)
        {
            Start = start;
            Length = length;
        }
        public int Start { get; set; }
        public int Length { get; set; }
    }
    

    And here is the content of the html template:

    <html>
        <head>
        <meta http-equiv="X-UA-Compatible" content="IE=11" />
        <script>
            function setCorrect(id){{document.getElementById(id).className = 'editable correct';}}
            function setWrong(id){{document.getElementById(id).className = 'editable wrong';}}
        </script>
        <style>
            div {{
                line-height: 1.5;
                font-family: calibri;
            }}
            .editable {{
                border-width: 0px;
                border-bottom: 1px solid #cccccc;
                font-family: monospace;
                display: inline-block;
                outline: 0;
                color: #0000ff;
                font-size: 105%;
            }}
            .editable.correct
            {{    
                color: #00ff00;
                border-bottom: 1px solid #00ff00;
            }}
            .editable.wrong
            {{    
                color: #ff0000;
                border-bottom: 1px solid #ff0000;
            }}
            .editable::-ms-clear {{
                width: 0;
                height: 0;
            }}
        </style>
        </head>
        <body>
        <div>
        {0}
        </div>
        </body>
    </html>
    
    0 讨论(0)
提交回复
热议问题