Home > Back-end >  Trying to figure out how to dynamically change which array is used for a label based on combo box se
Trying to figure out how to dynamically change which array is used for a label based on combo box se

Time:01-18

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.

Image of Program

So with the program, I've put a combo box that has an option for every piano scale which looks like this:

picture of combo box

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.

screenshot


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));
        }
    }
}
  • Related