Or maybe google is just not so friendly to me?
What I want is this simple thing:
constructor that accepts an array of menu item objects
Value get/set property that would set all the Checked properties right
bind to all Clicked events of the supplied items and provide One event
Working DataBind facilities
If you encountered such a nice thing around, please direct me. No need for manual do-it-in-your-form1.cs-class links, please. This I can do myself.
See: http://msdn.microsoft.com/en-us/library/ms404318.aspx
Summary: You'll have to make a new ToolStripMenuItem subclass that overrides the OnCheckChanged, OnOwnerChanged, and possibly OnPaint methods.
Note that in our case, we keep the check mark for the UI rather than a radio button. But keep the exclusive tick functionality.
OKay, here's my final code. It does something the other one doesn't (Supports binding), and vice versa. Perhaps one could combine. Use at your pleasure.
// Usage example:
//
// ric = new RadioItemCoupler(new ToolStripMenuItem[] {
// neverToolStripMenuItem,
// alwaysToolStripMenuItem,
// errorsOnlyToolStripMenuItem
// });
// this.Controls.Add(ric);
// _ric.DataBindings.Add("CheckedIndex", MySettings, "SmsReplyType",
// false, DataSourceUpdateMode.OnPropertyChanged);
public class RadioItemCoupler : Control
{
private int _checkedIndex;
// Zero-based
[Bindable(true)]
public int CheckedIndex
{
get { return _checkedIndex; }
set
{
_checkedIndex = value;
_items[value].Checked = true;
}
}
public event EventHandler CheckedIndexChanged;
ToolStripMenuItem[] _items;
private delegate void ItemHandler(ToolStripMenuItem item);
public RadioItemCoupler(ToolStripMenuItem[] items)
{
_items = items;
foreach (ToolStripMenuItem tsmi in _items)
{
tsmi.CheckOnClick = true;
tsmi.CheckedChanged += new EventHandler(tsmi_CheckedChanged);
}
}
void tsmi_CheckedChanged(object sender, EventArgs e)
{
ToolStripMenuItem that = sender as ToolStripMenuItem;
// Restore check if checked out
bool nothingChecked = true;
foreach(var item in _items)
nothingChecked = nothingChecked && !item.Checked;
if (nothingChecked)
{
_items[_checkedIndex].Checked = true;
return;
}
if (!that.Checked)
return;
for (int i = 0; i < _items.Length; i++)
{
if (that != _items[i])
{
if (_items[i].Checked)
_items[i].Checked = false;
}
else
{
_checkedIndex = i;
if (CheckedIndexChanged != null)
CheckedIndexChanged(this, EventArgs.Empty);
}
}
}
}
Related
I'm making a retro-style game with C# .NET-Framework, and for dialogue I'm using a for-statement, that prints my text letter by letter (like a typewriter-effect):
I'm working with different scenes, and I have a skip button (bottom right) that skips the current dialogue and passes to the next scene. My typewriter-effect automatically stops when all the text is displayed, but when I click on the skip button, it automatically skips to the next scene.
I would like it, when the typewriter is still active, and if I click on the skip button, that it first shows all the text, instead of skipping to the next scene.
So that it only skips to the next scene when all the text is displayed (automatically or manually).
This is the (working code) that I'm using for my typewriter method (+ variables):
public string FullTextBottom;
public string CurrentTextBottom = "";
public bool IsActive;
public async void TypeWriterEffectBottom()
{
if(this.BackgroundImage != null) // only runs on backgrounds that arent black
{
for(i=0; i < FullTextBottom.Length + 1; i++)
{
CurrentTextBottom = FullTextBottom.Substring(0, i); // updating current string with one extra letter
LblTextBottom.Text = CurrentTextBottom; // "temporarily place string in text box"
await Task.Delay(30); // wait for next update
#region checks for IsActive // for debugging only!
if(i < FullTextBottom.Length + 1)
{
IsActive = true;
Debug1.Text = "IsActive = " + IsActive.ToString();
}
if(CurrentTextBottom.Length == FullTextBottom.Length)
{
IsActive = false;
Debug1.Text = "IsActive = " + IsActive.ToString();
}
#endregion
}
}
}
And this is the code that I want to get for my skip button (named Pb_FastForward):
private void PbFastForward_Click(object sender, EventArgs e)
{
if( //typewriter is active)
{
//print all text into the textbox
}
else if( //all text is printed)
{
// skip to the next scene
}
}
But I don't know how to formulate the 2nd part of code. I've tried many different approaches, like using counters that increase on a buttonclick (and using that to check in an if-statement), and many different types of if-statements to see if the typewriter is still active or not, but I haven't got anything to work yet.
Edit
This is the sequence in which different components need to be loaded (on button click), which is related to the way different variables are updated:
Gamestate_Cycle() --> called for loading new scene.
FullTextBottom = LblTextBottom.Text --> called to refresh variables for typewriter.
TypeWriterEffectBottom() --> called to perform typewriter effect.
Avoid async void. Otherwise you can get an Exception that will break your game and you will not able to catch it.
Then use as less global variables in async methods as possible.
I suggest CancellationTokenSource as thread-safe way to stop the Type Writer.
public async Task TypeWriterEffectBottom(string text, CancellationToken token)
{
if (this.BackgroundImage != null)
{
Debug1.Text = "TypeWriter is active";
StringBuilder sb = new StringBuilder(text.Length);
try
{
foreach (char c in text)
{
LblTextBottom.Text = sb.Append(c).ToString();
await Task.Delay(30, token);
}
}
catch (OperationCanceledException)
{
LblTextBottom.Text = text;
}
Debug1.Text = "TypeWriter is finished";
}
}
Define CTS. It's thread-safe, so it's ok to have it in global scope.
private CancellationTokenSource cts = null;
Call TypeWriter from async method to be able to await it.
// set button layout as "Skip text" here
using (cts = new CancellationTokenSource())
{
await TypeWriterEffectBottom(yourString, cts.Token);
}
cts = null;
// set button layout as "Go to the next scene" here
And finally
private void PbFastForward_Click(object sender, EventArgs e)
{
if (cts != null)
{
cts?.Cancel();
}
else
{
// go to the next scene
}
}
I pondered on your task a bit more and it occurred to me that it is a good job for the Rx.Net library.
An advantage of this approach is that you have less mutable state to care about and you almost don't need to think about threads, synchronization, etc.; you manipulate higher-level building blocks instead: observables, subscriptions.
I extended the task a bit to better illustrate Rx capabilities:
there are two pieces of animated text, each one can be fast-forwarded separately;
the user can fast-forward to the final state;
the user can reset the animation state.
Here is the form code (C# 8, System.Reactive.Linq v4.4.1):
private enum DialogState
{
NpcSpeaking,
PlayerSpeaking,
EverythingShown
}
private enum EventKind
{
AnimationFinished,
Skip,
SkipToEnd
}
DialogState _state;
private readonly Subject<DialogState> _stateChanges = new Subject<DialogState>();
Dictionary<DialogState, (string, Label)> _lines;
IDisposable _eventsSubscription;
IDisposable _animationSubscription;
public Form1()
{
InitializeComponent();
_lines = new Dictionary<DialogState, (string, Label)>
{
{ DialogState.NpcSpeaking, ("NPC speaking...", lblNpc) },
{ DialogState.PlayerSpeaking, ("Player speaking...", lblCharacter) },
};
// tick = 1,2...
IObservable<long> tick = Observable
.Interval(TimeSpan.FromSeconds(0.15))
.ObserveOn(this)
.StartWith(-1)
.Select(x => x + 2);
IObservable<EventPattern<object>> fastForwardClicks = Observable.FromEventPattern(
h => btnFastForward.Click += h,
h => btnFastForward.Click -= h);
IObservable<EventPattern<object>> skipToEndClicks = Observable.FromEventPattern(
h => btnSkipToEnd.Click += h,
h => btnSkipToEnd.Click -= h);
// On each state change animationFarames starts from scratch: 1,2...
IObservable<long> animationFarames = _stateChanges
.Select(
s => Observable.If(() => _lines.ContainsKey(s), tick.TakeUntil(_stateChanges)))
.Switch();
var animationFinished = new Subject<int>();
_animationSubscription = animationFarames.Subscribe(frame =>
{
(string line, Label lbl) = _lines[_state];
if (frame > line.Length)
{
animationFinished.OnNext(default);
return;
}
lbl.Text = line.Substring(0, (int)frame);
});
IObservable<EventKind> events = Observable.Merge(
skipToEndClicks.Select(_ => EventKind.SkipToEnd),
fastForwardClicks.Select(_ => EventKind.Skip),
animationFinished.Select(_ => EventKind.AnimationFinished));
_eventsSubscription = events.Subscribe(e =>
{
DialogState prev = _state;
_state = prev switch
{
DialogState.NpcSpeaking => WhenSpeaking(e, DialogState.PlayerSpeaking),
DialogState.PlayerSpeaking => WhenSpeaking(e, DialogState.EverythingShown),
DialogState.EverythingShown => WhenEverythingShown(e)
};
_stateChanges.OnNext(_state);
});
Reset();
}
private DialogState WhenEverythingShown(EventKind _)
{
Close();
return _state;
}
private DialogState WhenSpeaking(EventKind e, DialogState next)
{
switch (e)
{
case EventKind.AnimationFinished:
case EventKind.Skip:
{
(string l, Label lbl) = _lines[_state];
lbl.Text = l;
return next;
}
case EventKind.SkipToEnd:
{
ShowFinalState();
return DialogState.EverythingShown;
}
default:
throw new NotSupportedException($"Unknown event '{e}'.");
}
}
private void ShowFinalState()
{
foreach ((string l, Label lbl) in _lines.Values)
{
lbl.Text = l;
}
}
private void Reset()
{
foreach ((_, Label lbl) in _lines.Values)
{
lbl.Text = "";
}
_state = DialogState.NpcSpeaking;
_stateChanges.OnNext(_state);
}
protected override void OnClosed(EventArgs e)
{
_eventsSubscription?.Dispose();
_animationSubscription?.Dispose();
base.OnClosed(e);
}
private void btnReset_Click(object sender, EventArgs e)
{
Reset();
}
I adjusted your code a little bit to achieve your goal. I'm not sure it's the best way to do it, but it should work.
public async void TypeWriterEffectBottom()
{
if(this.BackgroundImage == null)
{
return;
}
IsActive = true;
for(i=0; i < FullTextBottom.Length && IsActive; i++)
{
CurrentTextBottom = FullTextBottom.Substring(0, i+1);
LblTextBottom.Text = CurrentTextBottom;
await Task.Delay(30);
Debug1.Text = "IsActive = " + IsActive.ToString();
}
IsActive = false;
}
private void PbFastForward_Click(object sender, EventArgs e)
{
if(IsActive)
{
LblTextBottom.Text = FullTextBottom;
IsActive = false;
return;
}
// IsActive == false means all text is printed
// skip to the next scene
}
UPD: Just noticed that Hans Kesting has suggested pretty much exactly this in his comment.
You write what skip / forward button does, so you control it. Just have a check if the length of written text is equal to text that supposed to be written and if yes move as usual if not just display the text in full have delay to be read and move on
I'm looking to create a custom DataGrid such that users can attach notes to each cell using a popup input box. Currently I've created a CustomDataGrid class, inheriting from DataGrid, with a ContextMenu that has an option to add a note. When the user chooses to add a note, I find the selected cell, an input box is opened and returns the response, and I store it in a list of lists of strings, where each list of string represents a row. This doesn't work all the time, however, because sometimes no cell is selected, and I get an error message saying: 'Object reference not set to an instance of an object.'. I'm thinking of creating a CustomDataGridCell class, inheriting from DataGridCell, which has its own ContextMenu and note string. The question is, how would I make all cells in my CustomDataGrid a CustomDataGridCell? Is there a better way to do this?
Here is my current CustomDataGrid class:
public class CustomDataGrid : DataGrid
{
MenuItem miAddNote;
List<List<string>> notes;
public CustomDataGrid()
{
notes = new List<List<string>>();
miAddNote = new MenuItem();
miAddNote.Click += MiAddNote_Click;
miAddNote.Header = "Add a note";
this.ContextMenu = new ContextMenu();
this.ContextMenu.Items.Add(miAddNote);
}
private void MiAddNote_Click(object sender, RoutedEventArgs e)
{
try
{
int rowIndex = this.SelectedIndex;
int colIndex = this.SelectedCells[0].Column.DisplayIndex;
InputBox ib = new InputBox(notes[rowIndex][colIndex]);
if (ib.ShowDialog() == true)
{
notes[rowIndex][colIndex] = ib.Response;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
protected override void OnLoadingRow(DataGridRowEventArgs e)
{
base.OnLoadingRow(e);
int numColumns = this.Columns.Count;
List<string> newRow = new List<string>();
for (int i = 0; i < numColumns; ++i)
{
newRow.Add("");
}
notes.Add(newRow);
}
}
The question is, how would I make all cells in my CustomDataGrid a CustomDataGridCell?
There is no easy way to to this I am afraid. And it is not really necessary to create a custom cell type just to get rid of the exception.
Is there a better way to do this?
You should simply check whether there are any selected cells before trying to access any:
private void MiAddNote_Click(object sender, RoutedEventArgs e)
{
int rowIndex = this.SelectedIndex;
if (rowIndex != -1 && SelectedCells != null && SelectedCells.Count > 0)
{
int colIndex = this.SelectedCells[0].Column.DisplayIndex;
InputBox ib = new InputBox(notes[rowIndex][colIndex]);
if (ib.ShowDialog() == true)
{
notes[rowIndex][colIndex] = ib.Response;
}
}
}
So my program is generating a bunch of buttons like so:
foreach (var subdir in dir.GetDirectories()) {
var path = subdir.Name;
var button = new Button {
Text = getFlavor(path) + "\t(" + path + ")",
Width = Width,
Height = 35,
Top = y
};
button.Click += buttonClick;
Controls.Add(button);
if (button.Text.Contains("Kittens")
i++;
}
I want to try something like this
if (i == 1) {
[Button.ThatContains("Kitten)].Click;
}
"ThatContains" is not a real method. How do I get references to buttons I've created programmatically ?
You could use OfType<Button> to find all buttons in the container control where you've added them(f.e. a Panel). Then a liitle bit LINQ power gives you the correct button(s):
var kittenButtons = panel.Controls.OfType<Button>()
.Where(btn => btn.Text.Contains("Kittens"));
foreach(Button btn in kittenButtons)
btn.PerformClick();
If you just want to click the first:
Button kittenButton = panel.Controls.OfType<Button>()
.FirstOrDefault(btn => btn.Text.Contains("Kittens"));
if(kittenButton != null)
kittenButton.PerformClick();
For what it's worth, here is also an extension method that returns controls recursively via deferred execution which allows to use only the first found Buttton or consume all down the road:
public static IEnumerable<T> GetChildControlsRecursive<T>(this Control root) where T : Control
{
if (root == null) throw new ArgumentNullException("root");
var stack = new Stack<Control>();
stack.Push(root);
while (stack.Count > 0)
{
Control parent = stack.Pop();
foreach (Control child in parent.Controls)
{
if (child is T)
yield return (T)child;
stack.Push(child);
}
}
yield break;
}
Now you can use similar code as above to get for example the first matching button or all:
var kittenButtons = this.GetChildControlsRecursive<Button>()
.Where(b => b.Text.Contains("Kittens"));
// search just until the first button is found
Button firstKittenButton = kittenButtons.FirstOrDefault();
if(firstKittenButton != null) firstKittenButton.PerformClick;
// loop all
foreach(Button btn in kittenButtons)
btn.PerformClick();
Either create a subclass of Button to store the information you want and instantiate that instead or use the Tag property
public class MyButton : Button
{
public int ButtonID { get; set; }
}
public class MyApplication
{
public void DoSomething()
{
int i; // todo: loop stuff
var button = new MyButton
{
Text = getFlavor(path) + "\t(" + path + ")",
Width = Width,
Height = 35,
Top = y,
ButtonID = i
};
}
}
Or why not cast the sender parameter of the button click event as a Button and check the text?
public class MyApplication
{
public void DoSomething()
{
var b = new Button();
b.Click += b_Click;
}
public void b_Click(object sender, EventArgs e)
{
Button b = (Button)sender;
switch (b.Text) {
case "Kittens":
return;
default:
return;
}
}
}
Something like this
var button = FirstOrDefault(y => y is Button && y.Text.Contains("Kittens"));
if(button != null)
button.PerformClick();
In order to get the references, you may need to what you would do with getting references of any other type - store them somewhere, which does not seem to be the case here at all. Normally, you would register your buttons for interaction from a user by attaching them to a Form. Assuming you're not doing this by the looks of your sample code, I'm going to recommend storing them into a Dictionary<string, Button>.
You could use a dictionary or you could use a simple recursive loop (in case you are sticking the buttons into different containers).
private bool ClickButton(string buttonName, Control control) {
if (control is Button && control.Text.Contains(buttonName) {
((Button)control)PerformClick();
return true;
}
if (control.HasChildren) {
foreach (Control childControl in control.Controls) {
if (ClickButton(buttonName, childControl)) {
return true;
}
}
}
return false;
}
Usage: ClickButton("Kittens", this);
Or you could use a dictionary, as some have suggested.
private Dictionary<string, Button> DynamicButtons = new Dictionary<string, Button>();
private void ClickDictionaryButton(string buttonName) {
var matches = DynamicButtons.Where(x => x.Key.Contains(buttonName));
foreach (var match in matches) {
match.Value.PerformClick();
}
}
Usage: ClickDictionaryButton("Kittens", this);
I want to disable a combobox, but at the same time I want to let the users see the other options available (that is, I want to enable the dropdown).
By default, when ComboBox.Enabled = false, the dropdown is also disabled (nothing happens when we click on the combobox).
My first thought is to leave it enabled and handle the ComboBox.SelectedIndex event to set it back to the default value (I will just need to gray it out in some way.)
I am wondering if there is any native functionality like this that I am missing, or if there would be other way of doing it.
Don't use a Combobox if you don't want the Combobox functionality. Use a ListView instead.
A "What You See Is What You Can't Get" Combobox seems a bad idea.
I suggest using ListBox instead.
It's a hacky workaround, but it should accomplish something similar to your request:
public partial class Form1 : Form
{
ComboBox _dummy;
public Form1()
{
InitializeComponent();
// set the style
comboBox1.DropDownStyle =
System.Windows.Forms.ComboBoxStyle.DropDownList;
// disable the combobox
comboBox1.Enabled = false;
// add the dummy combobox
_dummy = new ComboBox();
_dummy.Visible = false;
_dummy.Enabled = true;
_dummy.DropDownStyle = ComboBoxStyle.DropDownList;
this.Controls.Add(_dummy);
// add the event handler
MouseMove += Form1_MouseMove;
}
void Form1_MouseMove(object sender, MouseEventArgs e)
{
var child = this.GetChildAtPoint(e.Location);
if (child == comboBox1)
{
if (!comboBox1.Enabled)
{
// copy the items
_dummy.Items.Clear();
object[] items = new object[comboBox1.Items.Count];
comboBox1.Items.CopyTo(items, 0);
_dummy.Items.AddRange(items);
// set the size and position
_dummy.Left = comboBox1.Left;
_dummy.Top = comboBox1.Top;
_dummy.Height = comboBox1.Height;
_dummy.Width = comboBox1.Width;
// switch visibility
comboBox1.Visible = !(_dummy.Visible = true);
}
}
else if (child != _dummy)
{
// switch visibility
comboBox1.Visible = !(_dummy.Visible = false);
}
}
}
If using a ListBox as other answers suggested is not convenient. There is a way by creating a custom combobox and adding a ReadOnly property. Try this code :
class MyCombo : System.Windows.Forms.ComboBox
{
public bool ReadOnly { get; set; }
public int currentIndex;
public MyCombo()
{
currentIndex = SelectedIndex ;
}
protected override void OnSelectedIndexChanged(EventArgs e)
{
if (ReadOnly && Focused)
SelectedIndex = currentIndex;
currentIndex = SelectedIndex;
base.OnSelectedIndexChanged(e);
}
}
Usually the background color of read-only controls should not change, so I haven't done that part.
I create a label "label1" dynamically in a method. Then when I click a button I want to remove that label created but if I write Controls.Remove(label1) it says that the control doesn't exist in the context.
How could I do to achieve this?
EDIT: Following Jon suggestion I implemented the foreach loop but it doesn't do anything. This is my code, the panel which I use is created by design:
void GenerateControls() {
Label labelOne = new Label();
Button btnContinue = new Button();
panel.SuspendLayout();
SuspendLayout();
//btnContinue
btnContinue.BackColor = System.Drawing.Color.Black;
btnContinue.ForeColor = System.Drawing.SystemColors.Menu;
btnContinue.Location = new System.Drawing.Point(145, 272);
btnContinue.Name = "btnContinue";
btnContinue.Size = new System.Drawing.Size(95, 28);
btnContinue.TabIndex = 13;
btnContinue.Text = "Continue";
btnContinue.Visible = true;
Controls.Add(btnContinue);
btnContinue.Click += new System.EventHandler(btnContinue_Click);
//labelOne
labelOne.Location = new Point(0,65);
labelOne.Size = new System.Drawing.Size(100,20);
labelOne.Text = "labelOne";
labelOne.Name = "labelOne";
labelOne.Visible = true;
labelOne.TextChanged += new System.EventHandler(this.lbl_TextChanged);
labelOne.BackColor = System.Drawing.Color.PaleGreen;
Controls.Add(labelOne);
//panel
panel.Controls.Add(labelOne);
panel.Visible = true;
panel.Location = new Point(0,0);
panel.Size = new Size(240, 320);
//
Controls.Add(panel);
panel.ResumeLayout();
ResumeLayout();
}
And then in when I click on btnContinue:
private void btnContinuar_Click(object sender, EventArgs e) {
foreach (Control control in panel.Controls) {
if (control.Name == "labelOne"){
panel.Controls.Remove(control);
break;
}
}
}
I debug it and in the panel.Control it continues as if it were empty panel.
Thanks for your help!
I suspect it says the variable doesn't exist in that context. You'll have to find the label by its text, or knowing something else about it. For example, when you create it you could set the Name property and find it by that when you want to remove it:
panel.Controls.RemoveByKey("YourLabelName");
EDIT: As noted in the comments, RemoveByKey doesn't exist in the compact framework. So you'd either have to remember the reference yourself (in which case you don't need the name) or use something like:
foreach (Control control in panel.Controls)
{
if (control.Name == "YourLabelName")
{
panel.Controls.Remove(control);
break;
}
}
EDIT2: And to make it even more "generic" and desktop compatible, you could keep the RemoveByKey call and add this to your app:
public static class FormExtensions
{
public static void RemoveByKey(this Control.ControlCollection collection,
string key)
{
if(!RemoveChildByName(collection, key))
{
throw new ArgumentException("Key not found");
}
}
private static bool RemoveChildByName(
this Control.ControlCollection collection,
string name)
{
foreach (Control child in collection)
{
if (child.Name == name)
{
collection.Remove(child);
return true;
}
// Nothing found at this level: recurse down to children.
if (RemoveChildByName(child.Controls, name))
{
return true;
}
}
return false;
}
}
After 20 edits to the OP question, and Jon's answer with no resemblance to the original problem, you are left with one small glitch.
Your Not adding labelOne to the panel you are adding it to the Form.
Change
Controls.Add(labelOne);
to
panel.Controls.Add(labelOne);
Then everything should work