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

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

    One option is to use a Masked Textbox.

    In your example, you would set the mask to:

    "The quick brown fox jLLLed over the l\azy hound"
    

    Which would appear as:

    "The quick brown fox j___ed over the lazy hound"
    

    And only allow 3 characters (a-z & A-Z) to be entered into the gap. And the mask could be easily changed via code.

    EDIT: For convenience...

    Here is a list and description of masking characters

    (taken from http://www.c-sharpcorner.com/uploadfile/mahesh/maskedtextbox-in-C-Sharp/).

    0 - Digit, required. Value between 0 and 9.
    9 - Digit or space, optional.
    # - Digit or space, optional. If this position is blank in the mask, it will be rendered as a space in the Text property.
    L - Letter, required. Restricts input to the ASCII letters a-z and A-Z.
    ? - Letter, optional. Restricts input to the ASCII letters a-z and A-Z.
    & - Character, required.
    C - Character, optional. Any non-control character.
    A - Alphanumeric, required.
    a - Alphanumeric, optional.
    .  - Decimal placeholder.
    , - Thousands placeholder.
    : - Time separator.
    / - Date separator.
    $ - Currency symbol.
    < - Shift down. Converts all characters that follow to lowercase.
    > - Shift up. Converts all characters that follow to uppercase.
    | - Disable a previous shift up or shift down.
    \ - Escape. Escapes a mask character, turning it into a literal. "\\" is the escape sequence for a backslash.
    

    All other characters - Literals. All non-mask elements will appear as themselves within MaskedTextBox. Literals always occupy a static position in the mask at run time, and cannot be moved or deleted by the user.

    0 讨论(0)
  • 2020-12-16 12:22
        Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Me.Controls.Add(New TestTextBox With {.Text = "The quick brown fox j___ed over the l__y hound", .Dock = DockStyle.Fill, .Multiline = True})
    End Sub
    
    
    
    Public Class TestTextBox
        Inherits Windows.Forms.TextBox
        Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
            Dim S = Me.SelectionStart
            Me.SelectionStart = ReplceOnlyWhatNeeded(Me.SelectionStart, (Chr(e.KeyCode)))
            e.SuppressKeyPress = True ' Block Evrything 
        End Sub
    
    
        Public Overrides Property Text As String
            Get
                Return MyBase.Text
            End Get
            Set(value As String)
                'List Of Editable Symbols 
                ValidIndex.Clear()
                For x = 0 To value.Length - 1
                    If value(x) = DefaultMarker Then ValidIndex.Add(x)
                Next
                MyBase.Text = value
                Me.SelectionStart = Me.ValidIndex.First
            End Set
        End Property
    
        '---------------------------------------
        Private DefaultMarker As Char = "_"
        Private ValidIndex As New List(Of Integer)
        Private Function ReplceOnlyWhatNeeded(oPoz As Integer, oInputChar As Char) As Integer
            'Replece one symbol in string at pozition, in case delete put default marker
            If Me.ValidIndex.Contains(Me.SelectionStart) And (Char.IsLetter(oInputChar) Or Char.IsNumber(oInputChar)) Then
                MyBase.Text = MyBase.Text.Insert(Me.SelectionStart, oInputChar).Remove(Me.SelectionStart + 1, 1) ' Replece in Output String new symbol
            ElseIf Me.ValidIndex.Contains(Me.SelectionStart) And Asc(oInputChar) = 8 Then
                MyBase.Text = MyBase.Text.Insert(Me.SelectionStart, DefaultMarker).Remove(Me.SelectionStart + 1, 1) ' Add Blank Symbol when backspace
            Else
                Return Me.ValidIndex.First  'Avrything else not allow
            End If
            'Return Next Point to edit 
            Dim Newpoz As Integer? = Nothing
            For Each x In Me.ValidIndex
                If x > oPoz Then
                    Return x
                    Exit For
                End If
            Next
            Return Me.ValidIndex.First
        End Function
    
    End Class
    

    U Dont Need Label and text Box for this, u can do it in any display it in any string control. Only u need user input position, string what u wanna change with symbols as place holder and input character, is sample on text box, at key input so u number of controls is not imported. For long string copy u can always for each char.

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

    This may be overkill depending on how complex you want this to be, but a winforms web browser control (which is essentially MSIE running inside your Winforms app) can work as an editor where you control which parts are editable.

    Load your content with the editable parts tagged as such, e.g.:

    <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
    <head>
    <meta http-equiv="X-UA-Compatible" content="IE=10" />
    <style>
      span.spEditable { background-color: #f0f0f0; }
    </style>
    </head>
    <body>
    <div id="someText">The quick brown fox j<span contenteditable="true" class="spEditable">___</span>ed over the l<span contenteditable="true" class="spEditable">__</span>y hound</div>
    </body>
    </html>

    Another option, a bit more work to code but more lightweight in terms of memory/resources, would be to use a FlowLayoutPanel, add normal panels to the FlowLayoutPanel, and then put either labels or textboxes on those panels depending on if a panel represent a fixed or editable portion, and resize them to match the length of the content. You can use MeasureString to find out the width of the content in each label/textbox for resizing purposes.

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

    I've worked up a bit easier solution to understand that might help you get started at the very least (I didn't have time to play with multiple inputs in the same label, but I got it working correctly for 1).

    private void Form1_Load()
    {
                for (var i = 0; i < 20; i++)
                {
                    Label TemporaryLabel = new Label();
                    TemporaryLabel.AutoSize = false;
                    TemporaryLabel.Size = new Size(flowLayoutPanel1.Width, 50);
                    TemporaryLabel.Text = "This is a ______ message";
                    string SubText = "";
                    var StartIndex = TemporaryLabel.Text.IndexOf('_');
                    var EndIndex = TemporaryLabel.Text.LastIndexOf('_');
                    if ((StartIndex != -1 && EndIndex != -1) && EndIndex > StartIndex)
                    {
                        string SubString = TemporaryLabel.Text.Substring(StartIndex, EndIndex - StartIndex);
                        SizeF nSize = Measure(SubString);
                        TextBox TemporaryBox = new TextBox();
                        TemporaryBox.Size = new Size((int)nSize.Width, 50);
                        TemporaryLabel.Controls.Add(TemporaryBox);
                        TemporaryBox.Location = new Point(TemporaryBox.Location.X + (int)Measure(TemporaryLabel.Text.Substring(0, StartIndex - 2)).Width, TemporaryBox.Location.Y);
                    }
                    else continue;
                    flowLayoutPanel1.Controls.Add(TemporaryLabel);
                }
    } 
    

    EDIT: Forgot the to include the "Measure" method:

    private SizeF Measure(string Data)
    {
        using (var BMP = new Bitmap(1, 1))
        {
            using (Graphics G = Graphics.FromImage(BMP))
            {
                return G.MeasureString(Data, new Font("segoe ui", 11, FontStyle.Regular));
            }
        }
    }
    

    The result:

    Then you should be able to assign event handlers to the individual text boxes/name them for easier access later on when the user interacts with the given input.

    0 讨论(0)
  • 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<EditableLabel> labels = new List<EditableLabel>();
    
        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.

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

    Consider using a combination of a DataGridView and a Masked Cell column.

    On Edit Control Showing, you would change the mask of that particular row.

    Here's some code usage example that includes the grid and the unique masking for each row.

    Public Class Form1
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    
            Dim mec As New MaskedEditColumn
            mec.Mask = ""
            mec.DataPropertyName = "Data"
            Me.DataGridView1.Columns.Add(mec)
    
    
            Dim tbl As New Data.DataTable
            tbl.Columns.Add("Data")
            tbl.Columns.Add("Mask")
            tbl.Rows.Add(New Object() {"The quick brown fox j   ed over the lazy hound", "The quick brown fox jaaaed over the l\azy hound"})
            tbl.Rows.Add(New Object() {"    quick brown fox j   ed over the lazy hound", "aaa quick brown fox jaaaed over the l\azy hound"})
            tbl.Rows.Add(New Object() {"The       brown fox j   ed over the lazy hound", "The aaaaa brown fox jaaaed over the l\azy hound"})
            Me.DataGridView1.AutoGenerateColumns = False
            Me.DataGridView1.DataSource = tbl
        End Sub
    
        Private Sub DataGridView1_EditingControlShowing(sender As Object, e As DataGridViewEditingControlShowingEventArgs) Handles DataGridView1.EditingControlShowing
    
            If e.Control.GetType().Equals(GetType(MaskedEditingControl)) Then
    
                Dim mec As MaskedEditingControl = e.Control
                Dim row As DataGridViewRow = Me.DataGridView1.CurrentRow
                mec.Mask = row.DataBoundItem("Mask")
    
            End If
    
        End Sub
    End Class
    

    And the grid column, sourced from here: http://www.vb-tips.com/MaskedEditColumn.aspx

    Public Class MaskedEditColumn
        Inherits DataGridViewColumn
        Public Sub New()
            MyBase.New(New MaskedEditCell())
        End Sub
        Public Overrides Property CellTemplate() As DataGridViewCell
            Get
                Return MyBase.CellTemplate
            End Get
            Set(ByVal value As DataGridViewCell)
    
                ' Ensure that the cell used for the template is a CalendarCell.
                If Not (value Is Nothing) AndAlso
                    Not value.GetType().IsAssignableFrom(GetType(MaskedEditCell)) _
                    Then
                    Throw New InvalidCastException("Must be a MaskedEditCell")
                End If
                MyBase.CellTemplate = value
            End Set
        End Property
        Private m_strMask As String
        Public Property Mask() As String
            Get
                Return m_strMask
            End Get
            Set(ByVal value As String)
                m_strMask = value
            End Set
        End Property
        Private m_tyValidatingType As Type
        Public Property ValidatingType() As Type
            Get
                Return m_tyValidatingType
            End Get
            Set(ByVal value As Type)
                m_tyValidatingType = value
            End Set
        End Property
        Private m_cPromptChar As Char = "_"c
        Public Property PromptChar() As Char
            Get
                Return m_cPromptChar
            End Get
            Set(ByVal value As Char)
                m_cPromptChar = value
            End Set
        End Property
        Private ReadOnly Property MaskedEditCellTemplate() As MaskedEditCell
            Get
                Return TryCast(Me.CellTemplate, MaskedEditCell)
            End Get
        End Property
    End Class
    Public Class MaskedEditCell
        Inherits DataGridViewTextBoxCell
        Public Overrides Sub InitializeEditingControl(ByVal rowIndex As Integer,
            ByVal initialFormattedValue As Object,
            ByVal dataGridViewCellStyle As DataGridViewCellStyle)
            ' Set the value of the editing control to the current cell value.
            MyBase.InitializeEditingControl(rowIndex, initialFormattedValue,
                dataGridViewCellStyle)
            Dim mecol As MaskedEditColumn = DirectCast(OwningColumn, MaskedEditColumn)
            Dim ctl As MaskedEditingControl =
                CType(DataGridView.EditingControl, MaskedEditingControl)
            Try
                ctl.Text = Me.Value.ToString
            Catch
                ctl.Text = ""
            End Try
            ctl.Mask = mecol.Mask
            ctl.PromptChar = mecol.PromptChar
            ctl.ValidatingType = mecol.ValidatingType
        End Sub
        Public Overrides ReadOnly Property EditType() As Type
            Get
                ' Return the type of the editing contol that CalendarCell uses.
                Return GetType(MaskedEditingControl)
            End Get
        End Property
        Public Overrides ReadOnly Property ValueType() As Type
            Get
                ' Return the type of the value that CalendarCell contains.
                Return GetType(String)
            End Get
        End Property
        Public Overrides ReadOnly Property DefaultNewRowValue() As Object
            Get
                ' Use the current date and time as the default value.
                Return ""
            End Get
        End Property
        Protected Overrides Sub Paint(ByVal graphics As System.Drawing.Graphics, ByVal clipBounds As System.Drawing.Rectangle, ByVal cellBounds As System.Drawing.Rectangle, ByVal rowIndex As Integer, ByVal cellState As System.Windows.Forms.DataGridViewElementStates, ByVal value As Object, ByVal formattedValue As Object, ByVal errorText As String, ByVal cellStyle As System.Windows.Forms.DataGridViewCellStyle, ByVal advancedBorderStyle As System.Windows.Forms.DataGridViewAdvancedBorderStyle, ByVal paintParts As System.Windows.Forms.DataGridViewPaintParts)
            MyBase.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts)
        End Sub
    End Class
    Class MaskedEditingControl
        Inherits MaskedTextBox
        Implements IDataGridViewEditingControl
        Private dataGridViewControl As DataGridView
        Private valueIsChanged As Boolean = False
        Private rowIndexNum As Integer
        Public Property EditingControlFormattedValue() As Object _
            Implements IDataGridViewEditingControl.EditingControlFormattedValue
            Get
                Return Me.Text
            End Get
            Set(ByVal value As Object)
                Me.Text = value.ToString
            End Set
        End Property
        Public Function EditingControlWantsInputKey(ByVal key As Keys,
               ByVal dataGridViewWantsInputKey As Boolean) As Boolean _
               Implements IDataGridViewEditingControl.EditingControlWantsInputKey
            Return True
        End Function
        Public Function GetEditingControlFormattedValue(ByVal context _
            As DataGridViewDataErrorContexts) As Object _
            Implements IDataGridViewEditingControl.GetEditingControlFormattedValue
            Return Me.Text
        End Function
        Public Sub ApplyCellStyleToEditingControl(ByVal dataGridViewCellStyle As _
            DataGridViewCellStyle) _
            Implements IDataGridViewEditingControl.ApplyCellStyleToEditingControl
    
            Me.Font = dataGridViewCellStyle.Font
            Me.ForeColor = dataGridViewCellStyle.ForeColor
            Me.BackColor = dataGridViewCellStyle.BackColor
        End Sub
        Public Property EditingControlRowIndex() As Integer _
            Implements IDataGridViewEditingControl.EditingControlRowIndex
            Get
                Return rowIndexNum
            End Get
            Set(ByVal value As Integer)
                rowIndexNum = value
            End Set
        End Property
        Public Sub PrepareEditingControlForEdit(ByVal selectAll As Boolean) _
            Implements IDataGridViewEditingControl.PrepareEditingControlForEdit
            ' No preparation needs to be done.
        End Sub
        Public ReadOnly Property RepositionEditingControlOnValueChange() _
            As Boolean Implements _
            IDataGridViewEditingControl.RepositionEditingControlOnValueChange
            Get
                Return False
            End Get
        End Property
        Public Property EditingControlDataGridView() As DataGridView _
            Implements IDataGridViewEditingControl.EditingControlDataGridView
            Get
                Return dataGridViewControl
            End Get
            Set(ByVal value As DataGridView)
                dataGridViewControl = value
            End Set
        End Property
    
        Public Property EditingControlValueChanged() As Boolean _
            Implements IDataGridViewEditingControl.EditingControlValueChanged
            Get
                Return valueIsChanged
            End Get
            Set(ByVal value As Boolean)
                valueIsChanged = value
            End Set
        End Property
        Public ReadOnly Property EditingControlCursor() As Cursor _
            Implements IDataGridViewEditingControl.EditingPanelCursor
            Get
                Return MyBase.Cursor
            End Get
        End Property
        Protected Overrides Sub OnTextChanged(ByVal e As System.EventArgs)
            ' Notify the DataGridView that the contents of the cell have changed.
            valueIsChanged = True
            Me.EditingControlDataGridView.NotifyCurrentCellDirty(True)
            MyBase.OnTextChanged(e)
        End Sub
    End Class
    
    0 讨论(0)
提交回复
热议问题