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

前端 未结 10 441
谎友^
谎友^ 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:35

    This is how I would approach it. Split with regular expression the string and create separate labels for each of the sub-strings. Put all the labels in a FlowLayoutPanel. When a label is clicked, remove it and on the same position add the editing TextBox. When the focus is lost (or enter is pressed) remove the TextBox and put the Label back; set the text of the label to the text of the TextBox.

    First create custom UserControl like the following

    public partial class WordEditControl : UserControl
    {
        private readonly Regex underscoreRegex = new Regex("(__*)");
        private List labels = new List();
    
        public WordEditControl()
        {
            InitializeComponent();
        }
    
        public void SetQuizText(string text)
        {
            contentPanel.Controls.Clear();
            foreach (string item in underscoreRegex.Split(text))
            {
                var label = new Label
                {
                    FlatStyle = FlatStyle.System,
                    Padding = new Padding(),
                    Margin = new Padding(0, 3, 0, 0),
                    TabIndex = 0,
                    Text = item,
                    BackColor = Color.White,
                    TextAlign = ContentAlignment.TopCenter
                };
                if (item.Contains("_"))
                {
                    label.ForeColor = Color.Red;
                    var edit = new TextBox
                    {
                        Margin = new Padding()
                    };
                    labels.Add(new EditableLabel(label, edit));
    
                }
                contentPanel.Controls.Add(label);
                using (Graphics g = label.CreateGraphics())
                {
                    SizeF textSize = g.MeasureString(item, label.Font);
                    label.Size = new Size((int)textSize.Width - 4, (int)textSize.Height);
                }
            }
        }
    
        // Copied it from the .Designer file for the sake of completeness
        private void InitializeComponent()
        {
            this.contentPanel = new System.Windows.Forms.FlowLayoutPanel();
            this.SuspendLayout();
            // 
            // contentPanel
            // 
            this.contentPanel.Dock = System.Windows.Forms.DockStyle.Fill;
            this.contentPanel.Location = new System.Drawing.Point(0, 0);
            this.contentPanel.Name = "contentPanel";
            this.contentPanel.Size = new System.Drawing.Size(150, 150);
            this.contentPanel.TabIndex = 0;
            // 
            // WordEditControl
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.Controls.Add(this.contentPanel);
            this.Name = "WordEditControl";
            this.ResumeLayout(false);
    
        }
    
        private System.Windows.Forms.FlowLayoutPanel contentPanel;
    }
    

    This one accepts the quiz text then splits it with regex and creates the labels and the text boxes. If you are interested to know how to make the Regex return the matches and not only the substrings have a look here

    Then in order to take care of the transition between editing, I created an EditableLabel class. It looks like this

    class EditableLabel
    {
        private string originalText;
        private Label label;
        private TextBox editor;
    
        public EditableLabel(Label label, TextBox editor)
        {
            this.label = label ?? throw new ArgumentNullException(nameof(label));
            this.editor = editor ?? throw new ArgumentNullException(nameof(editor));
            originalText = label.Text;
    
            using (Graphics g = label.CreateGraphics())
            {
                this.editor.Width = (int)g.MeasureString("M", this.editor.Font).Width * label.Text.Length;
            }
    
            editor.LostFocus += (s, e) => SetText();
            editor.KeyUp += (s, e) =>
            {
                if (e.KeyCode == Keys.Enter)
                {
                    SetText();
                }
            };
    
            label.Click += (s, e) =>
            {
                Swap(label, editor);
                this.editor.Focus();
            };
        }
    
        private void SetText()
        {
            Swap(editor, label);
            string editorText = editor.Text.Trim();
            label.Text = editorText.Length == 0 ? originalText : editorText;
            using (Graphics g = label.CreateGraphics())
            {
                SizeF textSize = g.MeasureString(label.Text, label.Font);
                label.Width = (int)textSize.Width - 4;
            }
        }
    
        private void Swap(Control original, Control replacement)
        {
            var panel = original.Parent;
            int index = panel.Controls.IndexOf(original);
            panel.Controls.Remove(original);
            panel.Controls.Add(replacement);
            panel.Controls.SetChildIndex(replacement, index);
        }
    }
    

    You can use the custom UserControl by drag and dropping it from the designer (after you successfully build) or add it like this:

    public partial class Form1 : Form
    {
        private WordEditControl wordEditControl1;
        public Form1()
        {
            InitializeComponent();
            wordEditControl1 = new WordEditControl();
            wordEditControl1.SetQuizText("The quick brown fox j___ed over the l__y hound");
            Controls.Add(wordEditControl1)
        }
    }
    

    The end result will look like that:

    Pros and Cons

    What I consider good with this solution:

    • it's flexible since you can give special treatment to the editable label. You can change its color like I did here, put a context menu with actions like "Clear", "Evaluate", "Show Answer" etc.

    • It's almost multiline. The flow layout panel takes care of the component wrapping and it will work if there are frequent breaks in the quiz string. Otherwise you will have a very big label that won't fit in the panel. You can though use a trick to circumvent that and use \nto break long strings. You can handle \n in the SetQuizText() but I'll leave that to you :) Have in mind that id you don't handle it the label will do and that won't bind well with the FlowLayoutPanel.

    • TextBoxes can fit better. The editing text box that will fit 3 characters will not have the same with as the label with 3 characters. With this solution you don't have to bother with that. Once the edited label is replaced by the text box, the next Controls will shift to the right to fit the text box. Once the label comes back, the other controls can realign.

    What I don't like though is that all these will come for a price: you have to manually align the controls. That's why you see some magic numbers (which I don't like and try hard to avoid them). Text box does not have the same height as the label. That's why I've padded all labels 3 pixels on the top. Also for some reason that I don't have time to investigate now, MeasureString() does not return the exact width, it's tiny bit wider. With trial and error I realised that removing 4 pixels will better align the labels

    Now you say there will be 300 strings so I guess you mean 300 "quizes". If these are as small as the quick brown fox, I think the way multiline is handled in my solution will not cause you any trouble. But if the text will be bigger I would suggest you go with one of the other answers that work with multiline text boxes.

    Have in mind though that if this grows more complex, like for example fancy indicators that the text was right or wrong or if you want the control to be responsive to size changes, then you will need text controls that are not provided by the framework. Windows forms library has unfortunately remained stagnant for several years now, and elegant solutions in problems such as yours are difficult the be found, at least without commercial controls.

    Hope it helps you getting started.

提交回复
热议问题