WPF 上位机开发学习笔记 - 第四天
项目 : 多工位缩管机上位机控制系统
公司 : 江苏良裕科技有限公司
日期 : 2026-08-20
技术栈: WPF / .NET Framework 4.7.2 / SQLite / C# 7.3
今日学习概览
- SQLite 数据库初始化与
e_sqlite3.dll原生库问题排查 - 密码安全: 加盐(Salt) + HMACSHA256 加密
- 用户登录功能实现
- C# 事件(Event)与委托(Action) - 窗口间通信
- AppSession 全局状态管理
- MainWindow 底部导航栏设计与 Tag 属性
- Window vs UserControl + ContentControl 页面切换架构
- Lambda 表达式(匿名函数)
一、SQLite 数据库初始化
1.1 App.xaml.cs - 启动时初始化数据库
csharp
// App.xaml.cs
using Multi_Station_Process_Machine.Helpers;
using System;
using System.IO;
using System.Windows;
namespace Multi_Station_Process_Machine
{
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
try
{
// 初始化 SQLite 原生库(必须在任何数据库操作之前调用)
SQLitePCL.Batteries_V2.Init();
// 创建数据库表
DatabaseHelper.initialize();
}
catch (Exception ex)
{
// 启动失败时,写入 crash.log 方便排查
string log = AppDomain.CurrentDomain.BaseDirectory + "crash.log";
File.WriteAllText(log, ex.ToString());
MessageBox.Show("启动失败: " + ex.Message + "\n\n" + ex.StackTrace);
Shutdown();
}
}
}
}
关键点:
SQLitePCL.Batteries_V2.Init()必须在所有 SQLite 操作之前调用- 用
try-catch包裹,启动失败时把异常写入crash.log文件
1.2 DatabaseHelper.cs - 数据库连接与建表
csharp
// Helpers/DatabaseHelper.cs
using Microsoft.Data.Sqlite;
using System;
using System.IO;
namespace Multi_Station_Process_Machine.Helpers
{
internal class DatabaseHelper
{
private static string DbPath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Data", "Users.db");
private static string ConnectionString = $"Data Source={DbPath}";
public static SqliteConnection GetConnection()
{
string dir = Path.GetDirectoryName(DbPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
var conn = new SqliteConnection(ConnectionString);
conn.Open();
return conn;
}
public static void initialize()
{
using (var conn = GetConnection())
{
string sql = @"CREATE TABLE IF NOT EXISTS Users(
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
identity TEXT NOT NULL DEFAULT 'User',
salt TEXT NOT NULL,
encrypt_password TEXT NOT NULL,
phone_number TEXT NOT NULL UNIQUE,
create_time TEXT NOT NULL,
update_time TEXT NOT NULL
)";
new SqliteCommand(sql, conn).ExecuteNonQuery();
}
}
}
}
表结构说明:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
INTEGER | 自增主键 |
username |
TEXT | 用户名(唯一) |
identity |
TEXT | 用户身份,默认 User |
salt |
TEXT | 加密盐值(随机生成) |
encrypt_password |
TEXT | 加密后的密码 |
phone_number |
TEXT | 手机号(唯一) |
create_time |
TEXT | 创建时间 |
update_time |
TEXT | 更新时间 |
1.3 e_sqlite3.dll 原生库踩坑
问题现象 : 程序启动崩溃,报 DllNotFoundException
排查过程:
- 从
runtimes\win-x64\native\复制e_sqlite3.dll,但机器上 AnyCPU 实际运行 x86 进程,加载 x64 DLL 直接崩溃 - 改为
runtimes\win-x86\native\- 可以了 - 但放在
runtimes子目录下 - .NET Framework 4.7.2 找不到 - 最终方案 : 直接把
e_sqlite3.dll放到bin\Debug\根目录下(exe 同级)
教训 : .NET Framework 不会自动扫描
runtimes子目录,必须把原生 DLL 放在 exe 同级目录
二、密码安全: 加盐 + HMACSHA256
2.1 为什么不能明文存密码?
数据库被脱库后,明文密码直接泄露.即使用 MD5,彩虹表也能秒破.所以我们使用 加盐(Salt) + HMACSHA256 方案.
2.2 PasswordHelper.cs
csharp
// Helpers/PasswordHelper.cs
using System;
using System.Security.Cryptography;
using System.Text;
namespace Multi_Station_Process_Machine.Helpers
{
internal class PasswordHelper
{
// 生成随机盐值(32字节 = 256位)
public static string GenerateSalt()
{
byte[] salt = new byte[32];
using (var rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(salt);
return Convert.ToBase64String(salt);
}
}
// 使用 HMACSHA256 对密码进行哈希
public static string HashPassword(string password, string salt)
{
using (var hmac = new HMACSHA256(Convert.FromBase64String(salt)))
{
byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
return Convert.ToBase64String(hash);
}
}
// 验证密码是否正确
public static bool VerifyPassword(string password, string storedHash, string salt)
{
return HashPassword(password, salt) == storedHash;
}
}
}
加密流程图解:
注册时:
用户输入密码 "123456"
-> GenerateSalt() -> 随机盐值 "xK9mB3..."
-> HashPassword("123456", "xK9mB3...") -> 哈希值 "aWb2c..."
-> 数据库存储: salt="xK9mB3...", encrypt_password="aWb2c..."
登录时:
用户输入密码 "123456"
-> 从数据库取出 salt 和 encrypt_password
-> HashPassword("123456", "xK9mB3...") -> 重新计算哈希
-> 对比: 新哈希 == 数据库存储的哈希? -> 相等则密码正确
2.3 Users 模型 - 构造函数自动生成盐和哈希
csharp
// Models/Users.cs
using Multi_Station_Process_Machine.Helpers;
using System;
namespace Multi_Station_Process_Machine.Models
{
internal class Users
{
public int Id { get; set; }
public string Name { get; set; }
public string Identity { get; set; }
public string Salt { get; set; }
public string EncryptPassword { get; set; }
public string PhoneNumber { get; set; }
public string CreateTime { get; set; }
public string UpdateTime { get; set; }
// 无参构造(从数据库查询时用)
public Users() { }
// 注册用构造函数: 传入明文密码,自动生成盐和哈希
public Users(string name, string confirmPassword, string phoneNumber)
{
if (name.Length < 5 || name.Length > 10)
throw new ArgumentException(nameof(name), "用户名长度不能小于5或大于10位");
if (confirmPassword.Length < 6 || confirmPassword.Length > 12)
throw new ArgumentException(nameof(confirmPassword), "密码长度不能小于6或大于12");
if (phoneNumber.Length != 11)
throw new ArgumentException(nameof(phoneNumber), "手机号长度必须为11位");
Name = name;
Salt = PasswordHelper.GenerateSalt();
EncryptPassword = PasswordHelper.HashPassword(confirmPassword, Salt);
PhoneNumber = phoneNumber;
CreateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
UpdateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
}
}
设计亮点: 构造函数内部自动完成加密,调用者只需传明文密码,不需要关心加密细节
三、用户登录功能
3.1 UserService.cs - Login 方法
csharp
// Helpers/DatabaseService/UserService.cs
using Microsoft.Data.Sqlite;
using Multi_Station_Process_Machine.Models;
namespace Multi_Station_Process_Machine.Helpers.DatabaseService
{
internal class UserService
{
// 登录 - C# 7.0 元组返回值
public static (bool success, string message, Users user) Login(
string username, string password)
{
Users u = SearchAllSpecu(username);
if (u == null) return (false, "未存在该用户,请先注册", null);
bool exist = PasswordHelper.VerifyPassword(password, u.EncryptPassword, u.Salt);
if (!exist) return (false, "用户密码错误,请重新输入", null);
return (true, "登录成功", u);
}
// 根据用户名查询全部信息
private static Users SearchAllSpecu(string name)
{
using (var conn = DatabaseHelper.GetConnection())
{
var cmd = new SqliteCommand(
"SELECT * FROM Users WHERE username=@u", conn);
cmd.Parameters.AddWithValue("u", name);
var reader = cmd.ExecuteReader();
if (reader.Read())
{
Users u = new Users();
u.Id = reader.GetInt32(0);
u.Name = reader.GetString(1);
u.Identity = reader.GetString(2);
u.Salt = reader.GetString(3);
u.EncryptPassword = reader.GetString(4);
u.PhoneNumber = reader.GetString(5);
u.CreateTime = reader.GetString(6);
u.UpdateTime = reader.GetString(7);
return u;
}
return null;
}
}
}
}
C# 7.0 元组返回值 : Login() 一次返回三个值(成功标志、消息、用户对象),比 out 参数优雅得多
3.2 LoginWindow - 登录按钮事件
csharp
// Views/LoginWindow.xaml.cs
private void BtnLogin_Click(object sender, RoutedEventArgs e)
{
string username = TxtUsername.Text;
string password = TxtPassword.Password;
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
{
MessageBox.Show("请输入用户名和密码");
return;
}
var result = UserService.Login(username, password);
if (result.success)
{
// 把当前用户信息保存到全局状态
AppSession.CurrentUsername = result.user.Name;
AppSession.CurrentIdentity = result.user.Identity;
// 打开主窗口,关闭登录窗口
MainWindow main = new MainWindow();
main.Show();
this.Close();
}
else
{
MessageBox.Show(result.message);
}
}
四、C# 事件与委托 - 窗口间通信
4.1 问题引入
登录成功后 MainWindow 打开了,但注销时怎么办?
TopBar在MainWindow里面,它不能直接操作MainWindow的生命周期MainWindow也不知道谁会触发注销
解决方案 : 使用 C# 事件(Event) + 委托(Action) 解耦
4.2 什么是委托和事件?
委托(Delegate): 定义一种"方法的类型",比如"无参数无返回值的方法"
Action: 系统内置的委托类型,表示"无参数无返回值的方法"
事件(Event): 基于委托的安全机制,只能 += 或 -=,不能直接调用
Invoke: 触发事件,通知所有订阅者
4.3 通俗比喻
事件就像报社的"订阅系统":
1. 报社(AppSession)发布了一个事件:"注销请求"(LogoutRequested)
2. MainWindow 说: 我订阅了(+=),注销时我会处理
3. 有人触发注销(AppSession.Logout()) -> 相当于报社发报纸
4. MainWindow 收到通知 -> 打开登录窗口,关闭自己
4.4 AppSession.cs - 全局状态 + 注销事件
csharp
// Helpers/AppSession.cs
using System;
namespace Multi_Station_Process_Machine.Helpers
{
internal class AppSession
{
// 注销事件(任何地方都可以订阅)
public static event Action LogoutRequested;
// 当前登录用户信息
public static string CurrentUsername { get; set; }
public static string CurrentIdentity { get; set; }
// 触发注销
public static void Logout()
{
LogoutRequested?.Invoke();
}
}
}
?.Invoke() 的含义 : ?. 是空条件运算符,"如果有订阅者就调用,没有就跳过,不报空引用异常"
4.5 MainWindow 订阅事件
csharp
// MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
TopBar1.DisplayWelcome(AppSession.CurrentUsername, AppSession.CurrentIdentity);
// 订阅注销事件
AppSession.LogoutRequested += () =>
{
var login = new LoginWindow();
login.Show();
this.Close();
};
}
4.6 完整注销流程
用户点击"注销"
|
TopBar.BtnLogout_Click()
|
AppSession.Logout()
|
AppSession.LogoutRequested?.Invoke() <- 触发事件
|
MainWindow 的 Lambda 被执行:
-> new LoginWindow().Show()
-> this.Close()
|
回到登录窗口
4.7 TopBar 的 DisplayLogout 控制
csharp
// Views/Control/TopBar.xaml.cs
public void DisplayLogout(bool visible)
{
BtnLogout.Visibility = visible
? Visibility.Visible
: Visibility.Collapsed;
}
Visibility.Visible: 显示Visibility.Collapsed: 隐藏(不占空间)Visibility.Hidden: 隐藏(占空间)
五、MainWindow 底部导航栏
5.1 XAML 布局
xml
<!-- MainWindow.xaml -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60"/> <!-- 顶部 TopBar -->
<RowDefinition Height="*"/> <!-- 中间内容区(自适应) -->
<RowDefinition Height="60"/> <!-- 底部导航栏 -->
</Grid.RowDefinitions>
<control:TopBar x:Name="TopBar1" Grid.Row="0"/>
<ContentControl x:Name="ContentArea" Grid.Row="1"/>
<!-- 底部导航栏 - 7个按钮共用一个 Click 事件 -->
<StackPanel Grid.Row="2" Orientation="Horizontal"
VerticalAlignment="Center"
Background="{StaticResource PrimaryDarkBrush}">
<Button Content="手动操作" Tag="Manual" Click="Nav_Click" .../>
<Button Content="自动操作" Tag="Auto" Click="Nav_Click" .../>
<Button Content="系统设置" Tag="System" Click="Nav_Click" .../>
<Button Content="编程设定" Tag="Program" Click="Nav_Click" .../>
<Button Content="IO检测" Tag="IODetect" Click="Nav_Click" .../>
<Button Content="日志操作" Tag="Log" Click="Nav_Click" .../>
<Button Content="配方选择" Tag="Recipe" Click="Nav_Click" .../>
</StackPanel>
</Grid>
5.2 Button 的 Tag 属性 - 为什么用 Tag?
不好的做法 (7个方法,代码重复):
csharp
private void BtnManual_Click(object sender, RoutedEventArgs e) { ... }
private void BtnAuto_Click(object sender, RoutedEventArgs e) { ... }
private void BtnSystem_Click(object sender, RoutedEventArgs e) { ... }
// ... 还有 4 个
好的做法 (1个方法搞定7个按钮):
csharp
private void Nav_Click(object sender, RoutedEventArgs e)
{
Button btn = sender as Button; // 拿到触发事件的按钮
string tag = btn.Tag.ToString(); // 取出 Tag 值
switch (tag)
{
case "Manual": ContentArea.Content = new ManualPage(); break;
case "Auto": ContentArea.Content = new AutoPage(); break;
case "System": ContentArea.Content = new SystemPage(); break;
case "Program": ContentArea.Content = new ProgramPage(); break;
case "IODetect": ContentArea.Content = new IOPage(); break;
case "Log": ContentArea.Content = new OperationalLogPage(); break;
case "Recipe": ContentArea.Content = new RecipeSelectionPage(); break;
}
}
5.3 sender as Button 详解
csharp
private void Nav_Click(object sender, RoutedEventArgs e)
{
Button btn = sender as Button;
}
sender是object类型 - "谁触发了这个事件"as Button把它安全转换为 Button 类型(转换失败返回 null,不报异常)- 这是 WPF 中多控件共用一个事件处理函数的标准写法
六、ContentControl 页面切换
6.1 Window vs UserControl
| 特性 | Window | UserControl |
|---|---|---|
| 独立窗口 | 是 | 否 |
| 可以放在 ContentControl 中 | 否 | 是 |
| 有自己的标题栏 | 是 | 否 |
| 内存开销 | 高(新窗口) | 低(嵌入) |
结论 : 7 个子页面全部用 UserControl,放在 MainWindow 的 ContentControl 里切换
6.2 页面模板
xml
<!-- Views/Page/ManualPage.xaml -->
<UserControl x:Class="Multi_Station_Process_Machine.Views.Page.ManualPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Multi_Station_Process_Machine.Views.Page"
mc:Ignorable="d">
<Grid>
<StackPanel>
<TextBlock Text="手动操作内容区域" FontSize="70"/>
</StackPanel>
</Grid>
</UserControl>
csharp
// Views/Page/ManualPage.xaml.cs
namespace Multi_Station_Process_Machine.Views.Page
{
public partial class ManualPage : UserControl
{
public ManualPage()
{
InitializeComponent();
}
}
}
6.3 切换原理
csharp
ContentArea.Content = new ManualPage();
ContentArea(ContentControl)
-> 把旧的页面对象丢弃(GC 回收)
-> 把新的 ManualPage 放进去
-> ManualPage 的 XAML 内容填充到 ContentArea 的位置
-> 用户看到页面切换了
这就是 WPF 的内容模型 : ContentControl.Content 可以放任何 WPF 元素
七、Lambda 表达式(匿名函数)
7.1 语法
csharp
// 完整写法
(sender, e) => { /* 方法体 */ }
// 单行可以省略大括号
(sender, e) => someMethod();
// 无参数
() => { /* 方法体 */ }
7.2 实际应用
csharp
// 1. 事件订阅(无参 Lambda)
AppSession.LogoutRequested += () =>
{
var login = new LoginWindow();
login.Show();
this.Close();
};
// 2. 按钮点击(有参 Lambda)
BtnLogin.Click += (sender, e) =>
{
MessageBox.Show("被点击了");
};
// 3. LINQ 查询
var adults = users.Where(u => u.Age >= 18).ToList();
7.3 委托的本质
csharp
// Lambda 其实是委托的一种简写
Action action = () => Console.WriteLine("Hello");
// 事件本质就是委托
public static event Action LogoutRequested;
// Action 是系统内置的委托,表示"无参数无返回值的方法"
八、UIHelper 工具类
8.1 占位符提示(UpdateHint)
csharp
// 当输入框为空时显示提示文字,有内容时隐藏
public static void UpdateHint(string value, TextBlock hint)
{
hint.Visibility = string.IsNullOrEmpty(value)
? Visibility.Visible
: Visibility.Collapsed;
}
8.2 输入过滤
csharp
// 只允许输入字母、数字、英文句号
public static void InputFilter_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !Regex.IsMatch(e.Text, @"^[a-zA-Z0-9.]$");
}
// 只允许输入数字
public static void InputNumberFilter_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !Regex.IsMatch(e.Text, @"^[0-9]$");
}
8.3 密码一致性检查
csharp
// 两次密码输入是否一致,显示对号或叉号
internal static void CheckPasswordStatus(string password1, string password2,
TextBlock txtPasswordStatus)
{
if (string.IsNullOrEmpty(password1) || string.IsNullOrEmpty(password2))
{
txtPasswordStatus.Visibility = Visibility.Collapsed;
return;
}
if (password1 == password2)
{
txtPasswordStatus.Text = "V";
txtPasswordStatus.Foreground = (SolidColorBrush)
Application.Current.FindResource("SuccessGreenBrush");
txtPasswordStatus.Visibility = Visibility.Visible;
}
else
{
txtPasswordStatus.Text = "X";
txtPasswordStatus.Foreground = (SolidColorBrush)
Application.Current.FindResource("AccentRedBrush");
txtPasswordStatus.Visibility = Visibility.Visible;
}
}
注意 : Application.Current.FindResource("SuccessGreenBrush") 返回 object,需要强转为 SolidColorBrush
九、TopBar 控件完整代码
9.1 XAML
xml
<!-- Views/Control/TopBar.xaml -->
<UserControl x:Class="Multi_Station_Process_Machine.Views.Control.TopBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="60">
<Border Background="{StaticResource PrimaryDarkBrush}">
<Grid Margin="5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <!-- 公司名 -->
<ColumnDefinition Width="*"/> <!-- PLC状态+时间+欢迎 -->
<ColumnDefinition Width="Auto"/> <!-- WiFi+语言+注销 -->
</Grid.ColumnDefinitions>
<!-- 左侧: 公司名称 -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="G" FontSize="30" Foreground="{StaticResource AccentRedBrush}"/>
<TextBlock Text="江苏良裕科技有限公司" FontSize="30" FontWeight="Bold"
Foreground="{StaticResource TextWhiteBrush}" Margin="8,0,0,0"/>
</StackPanel>
<!-- 中间: PLC状态 + 时间 + 欢迎语 -->
<StackPanel Grid.Column="1" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Ellipse Width="15" Height="15" Fill="#4CAF50" Margin="0,0,6,0"/>
<TextBlock Text="MELSOFT FX5U 已连接" FontSize="15"
Foreground="{StaticResource TextWhiteBrush}"/>
<Border Width="1" Height="20" Background="{StaticResource InputBorderBrush}" Margin="20,0"/>
<TextBlock x:Name="TxtTime" FontSize="20" Foreground="{StaticResource TextGrayBrush}"/>
<Border Width="1" Height="20" Background="{StaticResource InputBorderBrush}" Margin="20,0"/>
<TextBlock x:Name="TxtWelcome" FontSize="20" Margin="20,0,0,0"
Foreground="{StaticResource AccentBlueBrush}"/>
</StackPanel>
<!-- 右侧: WiFi + 语言 + 注销/退出 -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock x:Name="TxtWifi" Text="WiFi" FontSize="20"
Foreground="{StaticResource TextWhiteBrush}" Margin="0,0,20,0"/>
<TextBlock Text="{DynamicResource Str_Language}" FontSize="20"
Foreground="{StaticResource TextWhiteBrush}" Margin="0,0,20,0"/>
<Button x:Name="BtnLogout" Content="{DynamicResource Str_Logout}"
Click="BtnLogout_Click" Visibility="Collapsed" .../>
<Button Content="{DynamicResource Str_Exit}" Click="ExitButton_Click" .../>
</StackPanel>
</Grid>
</Border>
</UserControl>
9.2 Code-Behind
csharp
// Views/Control/TopBar.xaml.cs
using Multi_Station_Process_Machine.Helpers;
namespace Multi_Station_Process_Machine.Views.Control
{
public partial class TopBar : UserControl
{
private TimeHelper timeHelper;
private NetworkHelper networkHelper;
public TopBar()
{
InitializeComponent();
timeHelper = new TimeHelper(TxtTime);
networkHelper = new NetworkHelper(TxtWifi);
}
// 控制注销按钮是否显示
public void DisplayLogout(bool visible)
{
BtnLogout.Visibility = visible
? Visibility.Visible
: Visibility.Collapsed;
}
// 显示欢迎信息
public void DisplayWelcome(string name, string identity)
{
TxtWelcome.Text = $"欢迎 {name}, 当前用户身份: {identity}";
}
// 注销按钮点击 -> 触发 AppSession 事件
private void BtnLogout_Click(object sender, RoutedEventArgs e)
{
AppSession.Logout();
}
// 退出按钮点击
public void ExitButton_Click(object sender, RoutedEventArgs e)
{
AppHelper.Exit();
}
}
}
十、今日项目文件结构
Multi_Station_Process_Machine/
├── App.xaml 资源字典(颜色、样式)
├── App.xaml.cs 启动初始化(SQLite)
├── MainWindow.xaml 主窗口(TopBar + 内容区 + 导航栏)
├── MainWindow.xaml.cs 导航切换逻辑
├── Helpers/
│ ├── AppSession.cs 全局状态 + 注销事件
│ ├── DatabaseHelper.cs SQLite 连接 + 建表
│ ├── PasswordHelper.cs 加盐 + HMACSHA256
│ ├── UIHelper.cs 输入过滤、密码校验
│ ├── TimeHelper.cs 实时时钟
│ ├── NetworkHelper.cs WiFi 监控
│ ├── AppHelper.cs 退出/注销工具
│ └── DatabaseService/
│ └── UserService.cs 注册 + 登录 + 查询
├── Models/
│ └── Users.cs 用户模型(自动加密)
├── Views/
│ ├── LoginWindow.xaml + .cs 登录窗口
│ ├── RegisterWindow.xaml + .cs 注册窗口
│ ├── Control/
│ │ └── TopBar.xaml + .cs 顶部栏控件
│ └── Page/
│ ├── ManualPage.xaml + .cs 手动操作(占位)
│ ├── AutoPage.xaml + .cs 自动操作(占位)
│ ├── SystemPage.xaml + .cs 系统设置(占位)
│ ├── ProgramPage.xaml + .cs 编程设定(占位)
│ ├── IOPage.xaml + .cs IO检测(占位)
│ ├── OperationalLogPage.xaml + .cs 日志操作(占位)
│ └── RecipeSelectionPage.xaml + .cs 配方选择(占位)
├── Resources/
│ └── zh-CN.xaml 中文语言包
└── Data/
└── Users.db SQLite 数据库(自动生成)
十一、今日踩坑总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 程序启动崩溃 | e_sqlite3.dll 版本不对(x64 vs x86) | 用 runtimes\win-x86\native\ 的 DLL |
| DLL 放 runtimes 目录无效 | .NET Framework 不扫描 runtimes 子目录 | 放到 exe 同级目录(bin\Debug) |
| OnStartup 没执行 | 没有调用 base.OnStartup(e) | 加上 base.OnStartup(e) |
| 密码明文存储不安全 | 数据库泄露风险 | 加盐 + HMACSHA256 |
| 7 个按钮要写 7 个事件 | 代码重复 | 用 Tag + switch 一个事件搞定 |
| 按钮背景色不显示 | WPF 默认 Button 模板覆盖 Background | 需要自定义 ControlTemplate |
十二、明日计划
- 完善 MainWindow 的 Home 页面内容(设备概览仪表盘)
- 修复按钮背景色不显示问题(自定义 ControlTemplate)
- 手动操作页面 - 基本控件布局
- 持续学习 WPF 数据绑定
"@
System.IO.File::WriteAllText(path, content, $utf8)
Write-Output "Blog written successfully!"