I'm trying to make a point buy system using numeric up/downs. Here's the idea:
There are six numeric up/downs, one for each trait (Strength, Dexterity, Constitution, Intelligence, Wisdom and Charisma). Each trait begins at 10 points. You can't bring a trait below 7 or above 18.
I'm a total noob, but I managed to do this:
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
numericUpDown1.Maximum = 18;
numericUpDown1.Minimum = 7;
}
I did this one six times. In my form, there is now six numeric up/downs. Now I'm trying to do something which is way too much for my minuscule knowledge.
I want a system in which the value of the six numeric up downs is combined and cannot be exceeded, which means that in this case, we would have 60 points and couldn't increase any score unless we decreased one. I would add 15 points to that "Point pool", so the user doesn't have to decrease a stat straight away, in order to increase another one.
Example: I have 1 point left, and my scores go as follows: 15, 15, 14, 10, 10, 10. I increase the third score by 1 point. I now have this:
15, 15, 15, 10, 10, 10.
Now I have nothing left, but I want my fourth score at 15 points. In order to achieve this, I have to decrease the fifth and sixth score until I have 5 points freed up. I now have this:
15, 15, 15, 15, 7, 8.
Having a Lil' box in my form to display how many points are left would be a cherry on top.
I did my best to explain this. Please take note that English is not my native language and I sometimes do struggle with it.
I'm clueless as to how I can achieve this, as I barely have any knowledge of C#. What would be the code missing ?
It would easier if you create a Character class
You could define defaults for each property in constructor, and individual methods to increase or decrease its points.
public class Character
{
private int totalPointsMax = 60;
private int maxPoints = 18;
private int minPoints = 7;
public int Strength { get; set; }
public int Dexterity { get; set; }
public int Constitution { get; set; }
public int Intelligence { get; set; }
public int Wisdom { get; set; }
public int Charisma { get; set; }
public Character()
{
// create new Character defaults...
Strength = 10;
Dexterity = 10;
Constitution = 10;
Intelligence = 10;
Wisdom = 10;
Charisma = 10;
}
private int GetTotalCharacterPoints()
{
return Strength + Dexterity + Constitution + Intelligence + Wisdom + Charisma;
}
//example of method to increase Strength
public void IncreaseStrength()
{
int availablePoints = totalPointsMax - GetTotalCharacterPoints();
if (availablePoints > 0 && Strength < maxPoints)
Strength++;
}
//example of method to decrease Strength
public void DecreaseStrength()
{
if (Strength >= minPoints)
Strength--;
}
//missing the other increase/decrease methods for the rest of features...
}
You just need to instance at beginning and your UI buttons only need to invoke the CharacterFoo.IncreaseStrength() or CharacterFoo.DecreaseWisdom() ... etc.
Also, with this option you can allways reuse this in any part of the game..
(ex: if your character finds any special potion .. then CharacterFoo.IncreaseStrength() )
Hope this helps...
There's a couple of ways to do it.
Firstly, move these outside of the event function:
numericUpDown1.Maximum = 18;
numericUpDown1.Minimum = 7;
My recommendation would be to set a variable with the maximum points:
private int MAX_POINTS = 60;
Then when one of them is changed, you can call another method that adds up all of the current boxes, and determines whether it is over the limit or not:
private bool TotalOfStatsIsOverLimit() {
int total = GetTotalOfStats();
return total > MAX_POINTS;
}
private int GetTotalOfStats() {
int total = 0;
// TODO: Go through each number box and add to the total
return total;
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e) {
if(TotalOfStatsIsOverLimit()) {
// TODO: Decrease the value of the stat box that was updated
}
}
One advantage of doing it this way, is that you can reuse the same event method numericUpDown1_ValueChanged for all 6 of your stat boxes.
If I were you, I would go with a class for character since it will greatly simplify your future works, though if you are new, it may be harder at the start.
Still, you can follow up the basic approach as follows:
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
private const int totalPoints = 60 + 15; // sum of each trait plus the pool bonus
public Form1()
{
InitializeComponent();
foreach (Control control in this.Controls)
{
if (control is NumericUpDown)
{
NumericUpDown numControl = control as NumericUpDown;
numControl.Minimum = 7;
numControl.Maximum = 18;
numControl.Value = 10;
numControl.ValueChanged += nud_ValueChanged;
lblPointsLeft.Text = "15"; // bonus points
}
}
}
private void nud_ValueChanged(object sender, EventArgs e)
{
int sum = (int)(nudCha.Value + nudCon.Value + nudDex.Value + nudInt.Value + nudStr.Value + nudWis.Value);
int pointsLeft = totalPoints - sum;
NumericUpDown nudSender = (NumericUpDown)sender;
if (pointsLeft < 0)
{
MessageBox.Show("No points left");
// restore last change
// undo the last change
nudSender.Value = nudSender.Value - 1;
pointsLeft++;
}
lblPointsLeft.Text = pointsLeft.ToString();
}
}
}
Here's some Pseudocode:
You want to create a variable to hold your current points. Create some labels to hold that variable, and make sure you do AJAX calls, otherwise every time you update you are going to be calling this from the server again. This is probably better done in Javascript/Jquery.
int pointsUsed = numericUpDown1.value + numericUpDown2.value + numericUpDown6.value; //add all 6 of your values.
//for your textbox:
label1.text = "points left is:"
label2.text = 75 - pointsUsed;
private void checkBox1_Click(Object sender, EventArgs e)
{//to add points
if (pointsUsed < 75)
{
numericUpDown1.Value += 1;
pointsUsed += 1;
}
}
Check MSDN NumericUpDown for more info.
Related
I'm currently working on an assignment to create a game of Connect 4 on a standard 7x6 grid using Visual Studio forms. I've searched on here already, but couldn't find a question that uses the same method of checking for a win that I am supposed to. I already have the architecture of the game completed; the board is fully functional, the pieces drop correctly, and already placed pieces can't be overwritten. However the catch of the assignment is that checking for a winner needs to implement an interface. This is the interface that's required:
interface Winner
{
//find winner in the specified direction starting from (c,r)
bool straightup(int c, int r);
bool straightdown(int c, int r);
bool left(int c, int r);
bool right(int c, int r);
bool diagleftup(int c, int r);
bool diagrightdown(int c, int r);
bool diagleftdown(int c, int r);
bool diagrightup(int c, int r);
//return true if there is winner
bool winner();
}
Here's the code for the board if it's needed:
bool isWinner, xTurn; //Keeps track if there is a winner and whose turn it is
int xWins, oWins; //Keeps track of each players' wins
//Keeps track of which row the next piece in the column will drop
int col1Drop, col2Drop, col3Drop, col4Drop, col5Drop, col6Drop, col7Drop;
Button[,] places; //Array of buttons on the board
public Form1()
{
InitializeComponent();
isWinner = false;
xTurn = true;
xWins = 0;
oWins = 0;
col1Drop = 0; col2Drop = 0; col3Drop = 0; col4Drop = 0; col5Drop = 0; col6Drop = 0; col7Drop = 0;
currentTurn.Text = "X";
winRecords.Text = "Record:" + Environment.NewLine + Environment.NewLine + "X Wins - 0"+ Environment.NewLine + "O Wins - 0";
//Initializes the array of buttons on the board, starting from left to right, bottom to top
places = new Button[,]{ { col1row1, col1row2, col1row3, col1row4, col1row5, col1row6 }, { col2row1, col2row2, col2row3, col2row4, col2row5, col2row6 }, { col3row1, col3row2, col3row3, col3row4, col3row5, col3row6 }, { col4row1, col4row2, col4row3, col4row4, col4row5, col4row6 }, { col5row1, col5row2, col5row3, col5row4, col5row5, col5row6 }, { col6row1, col6row2, col6row3, col6row4, col6row5, col6row6 }, { col7row1, col7row2, col7row3, col7row4, col7row5, col7row6 } };
}
private void col1row6_Click(object sender, EventArgs e) //Runs when top button on column is clicked, all other buttons in column are disabled
{
if(xTurn) //Plays if its X's turn
{
if (col1Drop < 6) //Places piece as long as there's still a spot available
{
places[0, col1Drop].BackgroundImage = GUI_Connect4.Properties.Resources.X;
places[0, col1Drop].BackgroundImageLayout = ImageLayout.Stretch; //Fills the next available row in the column
xTurn = false; //Changes it to next player's turn
currentTurn.Text = "O";
col1Drop++; //Moves to next avaiable column
}
}
else //Plays if it's O's turn
{
if (col1Drop < 6)
{
places[0, col1Drop].BackgroundImage = GUI_Connect4.Properties.Resources.O;
places[0, col1Drop].BackgroundImageLayout = ImageLayout.Stretch;
xTurn = true;
currentTurn.Text = "X";
col1Drop++;
}
}
} //This code is the same for each column, with values changed accordingly
How would I go about checking for a win, and where should the interface be implemented? I'm assuming it should just be implemented in a separate checkWinner method that runs whenever a piece is played.
I have a text-box with the intention of entering integers. This integer is supposed to affect all 6 track-bars I have connected with each other, with the track-bar's maximum value all become addends to the entered integer.
For example; If I were to enter 318 into the first text-box, all 6 track-bar's maximum should be 318. If one track-bar's value is 67, then all of the bar's maximum should be 251, and so forth.
I have written all the code for this except that all 6 Track-Bar's Maximum value will not change when starting the program from it's default status. I've tried changing the event to be public instead of private, and putting the code in the public Form1() instead of an event but to no avail. What do I do? Here is an example of an event for one of the sliders I have.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Foo();
}
public void Foo()
{
basetotal.Text = "600";
hpvalue.Text = "0";
attackvalue.Text = "0";
defensevalue.Text = "0";
speedvalue.Text = "0";
specialattackvalue.Text = "0";
specialdefensevalue.Text = "0";
hpslider.Minimum = 0;
hpslider.TickFrequency = 100;
hpslider.LargeChange = 1;
double total, hp, attack, defense, speed, spa, spd, max;
total = double.Parse(basetotal.Text);
hp = double.Parse(hpvalue.Text);
attack = double.Parse(attackvalue.Text);
defense = double.Parse(defensevalue.Text);
speed = double.Parse(speedvalue.Text);
spa = double.Parse(specialattackvalue.Text);
spd = double.Parse(specialdefensevalue.Text);
max = total - hp - attack - defense - speed - spa - spd;
hpslider.Maximum = (int)max;
attackslider.Maximum = (int)max;
defenseslider.Maximum = (int)max;
speedslider.Maximum = (int)max;
specialattackslider.Maximum = (int)max;
specialdefenseslider.Maximum = (int)max;
}
private void Form1_Load(object sender, EventArgs e)
{
}
public void hpslider_ValueChanged(object sender, EventArgs e)
{
Foo();
}
Me and my teacher cannot figure this out. It's a windows forms app with all the appropriate fields linked, this is my only issue.
Error 1 'double' does not contain a definition for 'Text' and no extension method
here's my code:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void calculate_Click(object sender, EventArgs e)
{
double total = 0; //total amount
double tip = 0; //tip percentage
double meal = 0; //meal amount
tip = Convert.ToDouble(tip.Text) / 100;
meal = Convert.ToDouble(meal.Text);
total = (meal * tip);
total.Text = "$ " + Convert.ToString(total);
}
private void Form1_Load(object sender, EventArgs e)
{
}
}
It looks like you named your class-level Textboxes the exact same as your method-scoped variables. Or at least, that's my best assumption without knowing what your textboxes are actually called.
Your problem is you are trying to find a property Text on a double, which certainly does not exist. If you actually did name your textboxes the same (which is legal in C#), you'll want to reference them by using this.total and this.tip when trying to set the Text property.
It's worth noting that I'm concerned for your education if your teacher is unable to debug this.
I think you are trying to display the result into a Textbox; but Here in the code you declared total as double. please use the textbox name here (total.Text) in place of the variable total; And also use .ToString() instead of .Text inside your Convert.ToDouble(
Few more tips for you:
use double.TryParse for converting string to double
use total.ToString("$#00.00"); to get the the number prefixed with a $ symbol and rounded to 2 digits after the decimal
Let me assume the UI elements are :
//txtTip be the Textbox for tip percentage
//txtMeal be the Textbox for meal amount
//txtTotal be the Textbox for Total Amount
Then your code will be like the following:
double total = 0; //total amount
double tip = 0; //tip percentage
double meal = 0; //meal amount
if(! double.TryParse(txtTip.Text,out tip))
{
// show conversion failed error
}
if(! double.TryParse(txtMeal.Text,out meal))
{
// show conversion failed error
}
tip = tip / 100;
total = (meal * tip);
txtTotal.Text =total.ToString("$#00.00") ;
I think this is a mistake of use local variable and global variable.
class Test
{
// global variable.
int va = 1;
int vb = 2;
public void local()
{
bool va = false; // local variable
Console.WriteLine(va); // va is bool here.use local value.
Console.WriteLine(vb); // vb is int, use global value.
}
}
in your code, you declared local variables.
public partial class Form1 : Form
{
private void calculate_Click(object sender, EventArgs e)
{
// declare local variables...
// overwrite global varables.
double total = 0; //total amount
double tip = 0; //tip percentage
double meal = 0; //meal amount
// maybe, you want to invoke textbox.Text.
// but, tip(TextBox) object is overwrote by double.
// double has not the Text property, throw exception.
tip = Convert.ToDouble(tip.Text) / 100;
meal = Convert.ToDouble(meal.Text);
total = (meal * tip);
total.Text = "$ " + Convert.ToString(total);
}
}
how to fix it? just declare different variable name, like this:
private void calculate_Click(object sender, EventArgs e)
{
// make the local variable name is different with global.
double d_total = 0; //total amount
double d_tip = 0; //tip percentage
double d_meal = 0; //meal amount
d_tip = Convert.ToDouble(tip.Text) / 100; // tip is TextBox here
d_meal = Convert.ToDouble(meal.Text);
d_total = (d_meal * d_tip);
total.Text = "$ " + Convert.ToString(d_total);
}
or use this like this:
private void calculate_Click(object sender, EventArgs e)
{
double total = 0; //total amount
double tip = 0; //tip percentage
double meal = 0; //meal amount
tip = Convert.ToDouble(this.tip.Text) / 100;
meal = Convert.ToDouble(this.meal.Text);
total = (meal * tip);
this.total.Text = "$ " + Convert.ToString(total);
}
public PbsWheel(AnimatedPictureBox.AnimatedPictureBoxs[] pbs, AnimatedPictureBox.AnimatedPictureBoxs pb, int delta,Label label2)
{
for (int i = 0; i < pbs.Length; i++)
{
if (delta > 0)
{
pbs[i].AnimateRate += 1/60 * 1000;
1/60 * 1000 is 60 frames per second ?
This is how i animate the pictureBoxes the images inside. Im using timer for each picturebox:
public class AnimatedPictureBoxs : PictureBox
{
public static bool images;
List<string> imageFilenames;
Timer t = new Timer();
public AnimatedPictureBoxs()
{
images = false;
AnimateRate = 100; //It's up to you, the smaller, the faster.
t.Tick += Tick_Animate;
}
public int AnimateRate
{
get { return t.Interval; }
set { t.Interval = value; }
}
public void Animate(List<string> imageFilenames)
{
this.imageFilenames = imageFilenames;
t.Start();
}
public void StopAnimate()
{
t.Stop();
i = 0;
}
int i;
private void Tick_Animate(object sender, EventArgs e)
{
if (images == true)
{
imageFilenames = null;
}
if (imageFilenames == null)
{
return;
}
else
{
try
{
if (i >= imageFilenames.Count)
{
i = 0;
}
else
{
Load(imageFilenames[i]);
i = (i + 1) % imageFilenames.Count;
}
}
catch (Exception err)
{
}
}
}
The rate is set to 100 what i want to do is to display and when i move the mouse wheel up down to change the speed of the images animate by frames per second.
pbs is array of pictureBoxes.
pbs[i].AnimateRate += 1/60 * 1000;
Now, AnimateRate is an integer property. It is very badly named. It is not a rate. It is a timer interval. In mathematical terms it is a period. Naming it rate makes it sound as though it will be a rate, or a frequency.
The mathematical relationship between period T and frequency f is:
T = 1/f
So, here's what you should do:
Rename the property as AnimationInterval.
When you need to convert a frequency (i.e. frame rate) to an interval use the formula above.
Note that you need to account for the fact that your frequencies are measured in frames per second, but your intervals are measured in milli-seconds. So your code should be:
pbs[i].AnimationInterval += 1000/60;
That looks very similar to what you had but there is a subtle difference. In mathematics, the formulae are identical. But in C#, the behaviour of the / operator depends on the types of its operands. You supply two integers and so / is integer division. And the result of 1/60 is zero. So your code does not modify the property.
I do think that you will need to modify your logic a little. As it stands, your raw data is an interval. But actually what you wish to control if frame rate. So I believe that you should maintain a variable that holds the frame rate. If you want to modify it, then make the modifications to the frame rate variable. And then set the interval like this:
pbs[i].AnimationInterval = 1000/frameRate;
I'm in process of developing a new front end for an old open source fluid modeling engine written in C. I am using C# and WPF. The apps network generation requires the user to draw a network of pipes, reservoirs, nodes, tanks, etc. The app is more or less a fancy version of paint ;)
Right now I have the graphics set up as follows. I have an embedded win-forms Panel that has mouseclick, mousemove, and paint events. Mouse click saves the clicked coordinates to an array in the singleton class and then triggers the paint event via invalidate();. The paint event then loops through the array and draws all the node coordinates in the array via: g.FillEllipse(x,y,20,20); The user can click a node and bring up a menu via a function I wrote called DoesPointExist(xCord, yCord);. It loops through the coordinates array and returns true if both xcord and ycord are within 5px of the clicked coordinates. A somewhat archaic solution but it seems to work well enough.
This has worked pretty well for me thus far. But in the future I am going to have to give each node (or circle on the Panel) more and more properties. Nothing fancy, just numerical values like elevation that must be associated with each node. Also, I'm going to need to add in an option to delete nodes at some point.
I could probably do this by setting all values of the deleted row to 0 and placing in if statement in the paintevents loop to not draw deleted points, or even figure out how to just get rid of the row period and shift all the others down.
My question is there a more intelligent and OOP type way to go about this? Looping through and array seems a little old fashioned, there must be a better way to make use of C# features. Is there some sort of object or class I could setup to make this process simpler? The array is fine but by the end its going to be 40-50 columns. My background is more based in functional type programming with lower level languages like C. My program seems very bare of objects and classes other than a singleton glass for global data.
I know there are different ways to skin the cat; but it's important that the code I write will be accessible and easy to modify for future engineers so I'd like to add as much within the OOP paradigm as possible. My current code is very functional... but not very tidy.
Paint event code:
private void wfSurface_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
Graphics g;
Graphics h;
g = wfSurface.CreateGraphics();
h = wfSurface.CreateGraphics();
epanet epa = epanet.GetInstance();
SolidBrush black = new SolidBrush(System.Drawing.Color.Black);
SolidBrush blue = new SolidBrush(System.Drawing.Color.Pink);
SolidBrush green = new SolidBrush(System.Drawing.Color.Green);
System.Drawing.Pen line = new System.Drawing.Pen(System.Drawing.Color.FromArgb(255, 0, 0, 0));
//Loop to draw vertical grid lines
for (int f = 50; f < 1100; f += 50)
{
e.Graphics.DrawLine(line, f, 0, f, 750);
}
//Loop to draw vertical grid lines
for (int d = 50; d < 750; d += 50)
{
e.Graphics.DrawLine(line, 0, d, 1100, d);
}
//Loop nodes, tanks, and resevoirs
for (int L = 1; L < index; L += 1)
{
g.FillEllipse(black, Convert.ToInt32(epa.newNodeArray[L, 0] - 8), Convert.ToInt32(epa.newNodeArray[L, 1] - 8), 19, 19);
h.FillEllipse(blue, Convert.ToInt32(epa.newNodeArray[L, 0] - 6), Convert.ToInt32(epa.newNodeArray[L, 1] - 6), 15, 15);
}
for (int b = 1; b < resIndex; b += 1)
{
g.FillRectangle(green, Convert.ToInt32(epa.ResArray[b, 0] - 8), Convert.ToInt32(epa.ResArray[b, 1] - 8), 16, 16);
}
for (int c = 1; c < tankIndex; c += 1)
{
g.FillRectangle(black, Convert.ToInt32(epa.tankArray[c, 0] - 8), Convert.ToInt32(epa.tankArray[c, 1] - 8), 20, 20);
g.FillRectangle(green, Convert.ToInt32(epa.tankArray[c, 0] - 6), Convert.ToInt32(epa.tankArray[c, 1] - 6), 16, 16);
}
}
Code for click event:
private void wfSurface_MouseClick(object sender, System.Windows.Forms.MouseEventArgs e)
{
//Initialize epanet and save clicked coordinates to singleton class
epanet epa = epanet.GetInstance();
epa.xCord = e.X;
epa.yCord = e.Y;
//Check if point exists, if does open property window, doesn't do whatever drawing control is selected
if (epa.DoesPointExist(e.X, e.Y, index) == false)
{
switch (epa.controlSelected)
{
case "Node":
epa.newSetCords(index, e.X, e.Y);
wfSurface.Invalidate();
index += 1;
break;
case "Res":
epa.setResCords(resIndex, e.X, e.Y);
wfSurface.Invalidate();
resIndex += 1;
wfPanel.Cursor = Cursors.Arrow;
break;
case "Tank":
epa.setTankCords(tankIndex, e.X, e.Y);
wfSurface.Invalidate();
tankIndex += 1;
break;
case "Pointer":
break;
default:
//epa.newSetCords(index, e.X, e.Y);
wfSurface.Invalidate();
break;
}
}
else if (epa.DoesPointExist(e.X, e.Y, index) == true)
{
MessageBox.Show("Point Already Exists");
if (epa.propOpen == false)
{
// Open control properties in right pannel
}
}
Code for singleton class:
public class epanet
{
private static epanet instance = new epanet();
private epanet() { }
public static epanet GetInstance()
{
return instance;
}
//Microsoft.Win32.SaveFileDialog save = new Microsoft.Win32.SaveFileDialog();
//Network Node Data
public int nodeIndex { get; set; }
public int newNodeIndex { get; set; }
public double xCord { get; set; }
public double yCord { get; set; }
public double x1Cord { get; set; }
public double y1Cord { get; set; }
public int selectedPoint { get; set; }
//public List<double> nodeList = new List<double>();
//Saving Data
public int fileCopyNum { get; set; }
public string filename { get; set; }
public string path { get; set; }
public string fullFileName { get; set; }
//Window Condition Data
public bool drawSurfStatus { get; set; }
public bool windowOpen { get; set; }
public bool OpenClicked { get; set; }
public bool saveASed { get; set; }
public bool newClicked { get; set; }
public bool propOpen { get; set; }
//Drawing Controls
public string controlSelected { get; set; }
//Declare Array to store coordinates
public double[,] nodeArray = new double[100000, 3];
public double[,] newNodeArray = new double[100000, 7];
public double[,] ResArray = new double[100000, 7];
public double[,] tankArray = new double[100000, 7];
public void newSetCords(int newNodeIndex, double xCord, double yCord)
{
newNodeArray[newNodeIndex, 0] = xCord;
newNodeArray[newNodeIndex, 1] = yCord;
newNodeArray[nodeIndex, 2] = nodeIndex;
}
public void setResCords(int newNodeIndex, double xCord, double yCord)
{
ResArray[newNodeIndex, 0] = xCord;
ResArray[newNodeIndex, 1] = yCord;
ResArray[nodeIndex, 2] = nodeIndex;
}
public void setTankCords(int newNodeIndex, double xCord, double yCord)
{
tankArray[newNodeIndex, 0] = xCord;
tankArray[newNodeIndex, 1] = yCord;
tankArray[nodeIndex, 2] = nodeIndex;
}
public void setCords(int nodeIndex, double xCord, double yCord)
{
nodeArray[nodeIndex, 0] = xCord;
nodeArray[nodeIndex, 1] = yCord;
//nodeArray[nodeIndex, 2] = nodeIndex;
}
public bool DoesPointExist(double xcord, double ycord, int index)
{
int count = 1;
bool outcome = false;
while (count < index)
{
if (Math.Abs(xcord - newNodeArray[count, 0]) < 20)
{
if (Math.Abs(ycord - newNodeArray[count, 1]) < 20 )
{
outcome = true;
selectedPoint = count;
index = 0;
}
}
count += 1;
}
return outcome;
}
As I said everything works completely fine. I'm just looking for some feedback on if there's a more professional way to go about doing this.
If you've done much programming in C, you should be familiar with the concept of a linked-list. It's much more useful than an array, because it lets you delete from any point in the list in constant time. In C#, you'll want to look into System.Collections.Generic.List to make use of it.
Beyond that, standard object-oriented design is to look through your functional requirements and look for nouns, and those should become your classes. The biggest noun I see in your question is "node." So instead of having an array of vertices, have a linked list of nodes. Each node can have properties such as Coordinates and Elevation, and is then extensible if you need more. Note, if you find your Node class getting cumbersome, that's a huge sign that you should split off other classes. Any logical grouping should pretty coherently present itself.
Look through your epanet class that you included. You have functionality to handle files (filename, is it save-as'd, etc). That should be turned in to a class (in a separate file) just to help keep the files maintainable. The sequence of arrays you have, all of the same primary dimension define your general node structure. Put those and your function that operate on those into your Node class. You already have logic that works; it's just not intuitive to the next reader. Spend a bit of time questioning your placement of all your code, and asking yourself if there are any logical groupings you can put together.
Your general flow is fine if you want to limit yourself to Winforms/GDI+, and you can obviously get along that way, but since you're programming in WPF, you may want to step up to actually using it while your project is still young. C# Transition between GDI+ and WPF gives a little bit of a heads-up as how to do so, particularly suggestion 5 of the accepted answer. There are tons of other references on the web as well. A quick Google search brought up Manual rendering of a WPF User Control and of course MSDN. I'll leave WPF advice up to others who know it better than I.
If you don't know yet if you want to use GDI or WPF, you definitely need to have a GDIRenderer class. Ideally, you should have an IRenderer interface and a GDIRenderer class that implements that interface, though I do find that practically, it's more convenient to make the interface when you have a second object that implements it. In any case, your Renderer class(es) should take a List of Node as input. (You currently have them global, but it's generally good to only let one part of code have access to what it needs to, i.e. avoid globals.) In your current code, GDIRenderer would simply contain and register your OnPaint function. But by doing this, you isolate all of your rendering into a single class (which should be in a file of its own). Then, should you decide to move on to WPF, you can substitue the GDIRenderer for a WPFRenderer, which could contain all the code needed to handle the OnRender functionality. You want to do all you can to separate your business logic from the way you render.