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
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.
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.
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.
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.
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:
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 \n
to 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.
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