一个类似油漆的Windows应用程序的设计注意事项
本文关键字:Windows 应用程序 注意事项 一个 | 更新日期: 2023-09-27 18:02:02
我正在为一个用C编写的旧的开源流体建模引擎开发一个新的前端,我使用c#和WPF。应用程序的网络生成需要用户绘制管道、水库、节点、储罐等网络。该应用程序或多或少是一个花哨版本的油漆;)
现在我有如下设置的图形。我有一个嵌入式的win-forms面板,它有鼠标点击、鼠标移动和绘制事件。鼠标单击将单击的坐标保存到单例类中的数组中,然后通过invalidate();
触发绘制事件。然后,paint事件循环遍历数组并通过以下方式绘制数组中的所有节点坐标:g.FillEllipse(x,y,20,20);
用户可以点击一个节点并通过我编写的函数DoesPointExist(xCord, yCord);
调出菜单。它循环遍历坐标数组,如果xcord和ycord都在被单击坐标的5px范围内,则返回true。这是一个有点过时的解决方案,但似乎效果很好。
我可能会通过将已删除行的所有值设置为0并在painteevents循环中放置if语句来不绘制已删除的点,或者甚至弄清楚如何删除行周期并将所有其他的移到下面来做到这一点。
我的问题是是否有一种更智能和面向对象的方式来处理这个问题?循环遍历和数组似乎有点过时,一定有更好的方法来利用c#的特性。是否有某种类型的对象或类,我可以设置使这个过程更简单?数组很好,但到最后它会有40-50列。我的背景更多的是基于函数类型编程,使用较低级的语言,如c。我的程序似乎非常缺乏对象和类,除了一个全局数据的单例玻璃。
我知道有不同的方法去剥猫的皮;但重要的是,我写的代码将是可访问的,并易于修改为未来的工程师,所以我想添加尽可能多的面向对象范式。我目前的代码是非常实用的…但不太整洁。
绘制事件代码:
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);
}
}
点击事件代码:
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
}
}
单例类代码:
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;
}
就像我说的,一切都很好。我只是在寻找一些反馈,如果有更专业的方法来做这件事。
如果您用C编写过很多程序,那么您应该熟悉链表的概念。它比数组有用得多,因为它允许你在常量时间内从列表的任何点删除。在c#中,您将需要查看System.Collections.Generic.List
以使用它。
除此之外,标准的面向对象设计是检查您的功能需求并查找名词,这些名词应该成为您的类。我在你的问题中看到的最大的名词是"节点"。所以不是一个顶点数组,而是一个节点链表。每个节点都可以拥有坐标和海拔等属性,如果需要,还可以扩展这些属性。注意,如果您发现您的Node类变得越来越麻烦,这是一个巨大的信号,表明您应该分离其他类。任何逻辑分组都应该非常连贯地呈现自己。
查看您包含的epanet类。你有处理文件的功能(文件名,是否已保存,等等)。应该将其提交给一个类(在单独的文件中),以帮助保持文件的可维护性。所有相同主维的数组序列定义了一般的节点结构。将它们和对它们进行操作的函数放入Node类中。你已经有了有效的逻辑;这对下一个读者来说并不直观。花一点时间质疑你所有代码的位置,问问你自己是否有任何可以放在一起的逻辑分组。
如果你想把自己限制在Winforms/GDI+中,你的一般流程是好的,你显然可以这样做,但因为你在WPF中编程,你可能想要在你的项目还很年轻的时候实际使用它。c#在GDI+和WPF之间的转换给出了一点关于如何做到这一点的提示,特别是被接受的答案中的建议5。网上还有很多其他的参考资料。在谷歌上搜索一下,就会看到手动渲染WPF用户控件,当然还有MSDN。我将把WPF的建议留给那些比我更了解它的人。
如果你还不知道你是想使用GDI还是WPF,你肯定需要一个GDIRenderer类。理想情况下,你应该有一个IRenderer接口和一个实现那个接口的GDIRenderer类,尽管我确实发现实际上,当你有第二个实现它的对象时,它更方便制作接口。在任何情况下,你的Renderer类都应该接受一个List of Node作为输入。(你目前有全局的,但通常最好只让一部分代码访问它所需要的,即避免全局。)在你当前的代码中,GDIRenderer只是包含和注册你的OnPaint函数。但是通过这样做,您可以将所有呈现隔离到单个类中(该类应该在自己的文件中)。然后,如果您决定继续使用WPF,您可以将GDIRenderer替换为WPFRenderer,其中可能包含处理OnRender功能所需的所有代码。您希望尽您所能将您的业务逻辑与您的呈现方式分离。