So I'm unfortunately a bit of a super noob at the moment haha so please bear with me if possible.
The program that I'm attempting to make is a piano note randomizer for the sake of memorizing notes/scales efficiently. I thought it'd be a fun little program to aid in both my piano journey and programming journey haha.
So with the program, I've put a combo box that has an option for every piano scale which looks like this:
I then went ahead and made an array for each of the scales and the notes for that scale like so:
//Arrays to store each scale
string[] cMajor = { "C", "D", "E", "F", "G", "A", "B", "C", };
string[] dMajor = { "D", "E", "F#", "G", "A", "B", "C#", "D", };
string[] eMajor = { "E", "F#", "G#", "A", "B", "C#", "D#", "E", };
string[] fMajor = { "F", "G", "A", "Bb", "C", "D", "E", "F", };
string[] gMajor = { "G", "A", "B", "C", "D", "E", "F#", "G", };
I then used a random number generator to use as the index for the arrays so that it would randomly select a number between 0 and 7 which would ideally select an array index at random which we could then pass into a label which would be the piano note that is displayed on screen.
//Number Generator to pass into the newNumber variable which will act as an index for the array
Random generator = new Random();
int newNumber = generator.Next(0, 7); // Generating a number that equates to the number of notes in a particular scale
I then entered code to check the combobox for the selected input, which would then create the label based off of what scale is selected:
if (comboBox1.SelectedIndex == 0)
{
OUTPUTLABEL1.Font = new Font("Segoe UI", 100);
OUTPUTLABEL1.Location = new Point(80, 5);
OUTPUTLABEL1.Text = cMajor[newNumber];
}
Everything works as intended for the most part. I can select the scale and the note pops up on screen, the issue is that NONE of this is dynamic lol. The scale selection is hard-coded into the program as you can see. And the scale of cMajor is also hardcoded as well.
The idea is that I can select a scale from the ComboBox and the text will dynamically change based on what scale is selected in the ComboBox. The only way I can think of to acomplish this would create a ridiculous amount of redundant if statements and I feel like there's probably a better way I could accomplish this.
Thank you guys so much and once again I do apologize if this is obvious or any other mistake that I might've made. This is my first post here and I hope that I've provided enough information for you guys! Thank you again!
CODE IN IT'S ENTIRETY (Ignore the timer):
public void RandomizeButton_Click(object sender, EventArgs e)
{
//Arrays to store each scale
string[] cMajor = { "C", "D", "E", "F", "G", "A", "B", "C", };
string[] dMajor = { "D", "E", "F#", "G", "A", "B", "C#", "D", };
string[] eMajor = { "E", "F#", "G#", "A", "B", "C#", "D#", "E", };
string[] fMajor = { "F", "G", "A", "Bb", "C", "D", "E", "F", };
string[] gMajor = { "G", "A", "B", "C", "D", "E", "F#", "G", };
//Number Generator to pass into the newNumber variable which will act as an index for the array
Random generator = new Random();
int newNumber = generator.Next(0, 7); // Generating a number that equates to the number of notes in a particular scale
if (comboBox1.SelectedIndex == 0)
{
OUTPUTLABEL1.Font = new Font("Segoe UI", 100);
OUTPUTLABEL1.Location = new Point(80, 5);
OUTPUTLABEL1.Text = cMajor[newNumber];
}
if (TimerCheckbox.Checked)
{
seconds = int.Parse(timeInterval.Text);
timer1.Start();
}
}
If my way of thinking with arrays and combo boxes is less than ideal then I'm open to an entire new solution if needed! Thank you guys so much!
CodePudding user response:
It looks like you want a linked data structure. The most common method to have a map between one variable and another is to use a Dictionary
//you can use a dictionary
public Dictionary<string, string[]> scalesDict = new Dictionary<string, string[]>() {
{"a", new string[] { "C", "D", "E", "F", "G", "A", "B", "C", } },
{"b", new string[] { "D", "E", "F#", "G", "A", "B", "C#", "D", } },
{"c", new string[] { "E", "F#", "G#", "A", "B", "C#", "D#", "E", } },
{"d", new string[] { "F", "G", "A", "Bb", "C", "D", "E", "F", } },
{"e", new string[] { "G", "A", "B", "C", "D", "E", "F#", "G", } } };
private string[] GetScale(string comboBoxValue) {
return scalesDict[comboBoxValue];
}
the above code would get you your array based on the combobox value (replace a,b,c,d, and e with whatever your combobox uses)
dictionaries are very powerful tools in c#
Your question was a bit vague though, so I'm not sure if I've fully covered your issue, let me know
CodePudding user response:
Your question and code cover a few related areas:
- Dynamically change which array (of notes) is used for the label based on combo box selection.
- Use the
Random
class correctly to implement the Randomize function. - Automate the Randomize button with a timer.
Ok, you did say I'm open to an entire new solution if needed! so this answer will explore other ways to go about these three things and give you a couple new skills to try out. In particular, if you learn how to use Data Binding early on (as shown in this sample) it's likely to accelerate everything else you ever do in WinForms.
Binding the Scales to the ComboBox
One approach is to define a class
that represents a Scale. This associates the information more tightly than a dictionary lookup would. The ToString
method will determine what's shown in the combo box.
class Scale
{
public string Name { get; set; } = string.Empty;
public string[] Notes { get; set; } = new string[0];
// Determines what will show in the ComboBox
public override string ToString() => Name;
}
What we do next is make a list of Scale
objects that will be the dynamic source of the combo box:
BindingList<Scale> Scales = new BindingList<Scale>();
Initialize the individual scales and the list of scales in the method that loads the main form.
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void onl oad(EventArgs e)
{
base.OnLoad(e);
Scales.Add(new Scale
{
Name = "C Major",
Notes = new[]{ "C", "D", "E", "F", "G", "A", "B", "C", },
});
Scales.Add(new Scale
{
Name = "D Major",
Notes = new[] { "D", "E", "F#", "G", "A", "B", "C#", "D", },
});
Scales.Add(new Scale
{
Name = "E Major",
Notes = new[] { "E", "F#", "G#", "A", "B", "C#", "D#", "E", },
});
Scales.Add(new Scale
{
Name = "F Major",
Notes = new[] { "F", "G", "A", "Bb", "C", "D", "E", "F", },
});
Scales.Add(new Scale
{
Name = "G Major",
Notes = new[] { "G", "A", "B", "C", "D", "E", "F#", "G", },
});
comboBoxScales.TabStop= false;
comboBoxScales.DropDownStyle= ComboBoxStyle.DropDownList;
// Attach the list of scales
comboBoxScales.DataSource= Scales;
// Initialize the value
onScaleSelectionChanged(this, EventArgs.Empty);
// Respond to combo box changes
comboBoxScales.SelectedIndexChanged = onScaleSelectionChanged;
// Respond to click randomize
buttonRandomize.Click = onClickRandomize;
// Respond to automated timer checkbox changes
checkBoxTimer.CheckedChanged = onTimerCheckedChanged;
}
.
.
.
}
Also in the same method, the event handlers are attached. For example, when a new scale is chosen in the combo box, the first thing that happens is that the label is set to the root.
private void onScaleSelectionChanged(object? sender, EventArgs e)
{
labelCurrentNote.Text =
((Scale)comboBoxScales.SelectedItem).Notes[0];
}
Using the Random
class
You only need one instance of Random
. For testing, you can produce the same pseudorandom sequence of numbers every time by seeding it with an int, e.g. new Random(1)
. But as shown here, the seed is derived from the system clock for a different sequence every time you run it.
private readonly Random _rando = new Random();
private void onClickRandomize(object? sender, EventArgs e) =>
execNextRandom(sender, e);
One implementation would be to get a number between 0 and 7 inclusive, and use it to dereference an array value from the current selection in the combo box. This also makes a point of getting a new not every click so the user feels confident when they click.
private void execNextRandom(object? sender, EventArgs e)
{
string preview;
do
{
// Randomize, but do not repeat because it makes
// it seem like the button doesn't work!
preview =
((Scale)comboBoxScales.SelectedItem)
.Notes[_rando.Next(0, 8)];
} while (preview.Equals(labelCurrentNote.Text));
labelCurrentNote.Text = preview;
}
Automated timer
One of the easier ways to get a repeating function is to make an async handler for the timer checkbox. There's no need to start and stop a Timer or handle events this way, but it still offers the advantages of keeping your UI responsive by 'not' blocking the thread except for when the new random note is acquired.
private async void onTimerCheckedChanged(object? sender, EventArgs e)
{
if(checkBoxTimer.Checked)
{
while(checkBoxTimer.Checked)
{
execNextRandom(sender, e);
await Task.Delay(TimeSpan.FromSeconds((double)numericUpDownSeconds.Value));
}
}
}