目前,根据前面winform做的上位机,我研究了下wpf下设计上位机,希望把界面做的更美观,目前实现了串口助手的功能,通讯协议初步支持了modbus和原本winform里面的协议,算法调取部分目前还未迁移完成,估计无限期延后,相关的源代码公开到github和gitee上面(搜索finhaz/fruit,里面的ocean工程就是wpf对应的设计),希望给大家提供思路。这里的思路主要是界面设计方面的思路,上位机设计的思想在前面的winform设计篇(点此)讲了,而上位机所需的通讯方面,则记录于modbus协议那篇博文(点此)。
wpf做的软件,表面上至少比winform的漂亮了,而且自适应框架的能力更强,就是相对费事。
目前不想继续开发下去的主要原因是我要去搞学术研究了,时间精力有限,暂时使用mthings作为调试用上位机。
之前我在winform的设计思路下,主要考虑的是利用多个窗体,来管理多个不同的输入界面,这样在刚开始设计的时候很简单。
而在换了wpf进行界面设计时,我考虑的是做一个窗体,然后在页面之间切换,来选择不同的界面,并且切换界面使用的导航栏我考虑的是使用汉堡菜单。
同时,算法开发与界面设计分离,采用MVVM模式。
也就是软件本身一个mainwindows,对应xaml文件如下:
每个页面page对应一个page:
用汉堡菜单切换各个页面。
wpf的最方面的一个地方是利用xaml可以快速的实现自适应页面,有gird等控件可以辅助使用,网上相关界面很多,这里不再赘述。
在wpf里面想实现汉堡菜单时,没有原生的控件,因此这时候,通过查询网上,了解到有一个设计框架库MahApps.Metro。MahApps用户UI库帮助把uwp的特性可以被移植到wpf开发上,提供了很多控件/样式,利用Nuget可以安装该类库。
这里初步的界面效果是这样的
左侧是导航栏,点击按钮就会跳转到对应的页面,即每个page对应的xaml文件。
修改对应的xaml文件的方法,在ShellViewModel.cs之中,修改对应的xaml文件路径和名称
// Build the menus
this.Menu.Add(new MenuItem()
{
Icon = new PackIconFontAwesome() { Kind = PackIconFontAwesomeKind.BugSolid },
Label = "Bugs",
NavigationType = typeof(BugsPage),
NavigationDestination = new Uri("UI/debug_serial.xaml", UriKind.RelativeOrAbsolute)
});
像导航菜单、上位机设计时需要读取的数据表,我建立了一个CommonRes类
public class CommonRes
{
public static SerialPort mySerialPort = new SerialPort();
public static DataTable dt1 = new DataTable();
public static DataTable dt2 = new DataTable();
public static DataTable dt3 = new DataTable();
}
初始化时集中在了mainwindows对应的cs代码之中。
this.navigationServiceEx = new Navigation.NavigationServiceEx();
this.navigationServiceEx.Navigated += this.NavigationServiceEx_OnNavigated;
this.HamburgerMenuControl.Content = this.navigationServiceEx.Frame;
// Navigate to the home page.
this.Loaded += (sender, args) => this.navigationServiceEx.Navigate(new Uri("Views/MainPage.xaml", UriKind.RelativeOrAbsolute));
CommonRes.dt1 = DB_Access.GetDBTable("PARAMETER_RUN");
CommonRes.dt2 = DB_Access.GetDBTable("PARAMETER_SET");
CommonRes.dt3 = DB_Access.GetDBTable("PARAMETER_FACTOR");
这样各个page读取时,就可跨越所有的页面使用。
这里对应上位机设计中的数据库部分,是比较关键的地方
读取access数据的方式和之前一样,相关的操作函数我写在database.cs文件之中。
CommonRes.dt1 = DB_Access.GetDBTable("PARAMETER_RUN");
CommonRes.dt2 = DB_Access.GetDBTable("PARAMETER_SET");
CommonRes.dt3 = DB_Access.GetDBTable("PARAMETER_FACTOR");
对应的显示控件是DataGrid,这里我是使用数据绑定的方式,让每个DataGrid绑定到一个datatable变量
<DataGrid x:Name="datashow"
CellEditEnding="datashow_CellEditEnding"
BeginningEdit="datashow_BeginningEdit"
ItemsSource="{Binding dtrun, Mode=TwoWay}">
</DataGrid>
access读取的值赋值给page内的datatable,这样就把数据库内容显示到DataGrid之上。
dtrun = CommonRes.dt1;
dtset = CommonRes.dt2;
dtfactor = CommonRes.dt3;
DataGrid控件相比于winform的 datagridview,在数据操作上复杂,可利用datagrid变量的select_index属性来读取每个datagrid对应上你当前cell的所在行,返回值而且是int类型。
var x = dataset.SelectedIndex;
每次编辑后,利用单元格的endedit事件,将修改的结果返回给数据库读取时的datatable变量
private void dataset_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
newValue = (e.EditingElement as TextBox).Text;
CommonRes.dt2 = dtset;
}
另外,增加了窗口关闭的事件,将datatable变量值用于更新表
MessageBox.Show("数据将保存!");
//DB_Access.UpdateDBTable(CommonRes.dt1, "PARAMETER_RUN");
DB_Access.UpdateDBTable(CommonRes.dt2, "PARAMETER_SET");
DB_Access.UpdateDBTable(CommonRes.dt3, "PARAMETER_FACTOR");
这里,上位机的串口和UI是两个线程,通常我们会把报文数据显示到textbox的控件,这时候就需要委托了。我的textbox叫show_text,每个控件有Dispatcher属性,利用这一属性编写output函数,加载ouputAction操作。
private delegate void outputDelegate(string para);
private void output(string para)
{
this.show_text.Dispatcher.Invoke(new outputDelegate(outputAction), para);
}
private void outputAction(string para)
{
show_text.Text+=para;
}
操作控件时,使用的就是output函数在串口接受事件里面,传入参数就是你想要显示的数据。
private void mySerialPort_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
Thread.Sleep(99);
int n = CommonRes.mySerialPort.BytesToRead;
byte[] buf = new byte[n];
CommonRes.mySerialPort.Read(buf, 0, n);
string txt = "RX:";
for (int i = 0; i < n; i++)
{
if (Protocol_num == 0)
{
txt += Convert.ToString(buf[i], 16);
}
else if (Protocol_num == 1)
{
txt += Convert.ToString(buf[i], 16);
}
txt += ' ';
}
txt += '\r';
txt += '\n';
output(txt);
}
由于我有调试串口和通信协议两个page都用到了串口,接受处理事件不一样,因此我在页面切换时,加了-=。
private void Page_Unloaded(object sender, RoutedEventArgs e)
{
CommonRes.mySerialPort.DataReceived -= new SerialDataReceivedEventHandler(this.mySerialPort_DataReceived);
}