WPF 上位机开发学习笔记 - 第四天

WPF 上位机开发学习笔记 - 第四天

项目 : 多工位缩管机上位机控制系统

公司 : 江苏良裕科技有限公司

日期 : 2026-08-20

技术栈: WPF / .NET Framework 4.7.2 / SQLite / C# 7.3


今日学习概览

  1. SQLite 数据库初始化与 e_sqlite3.dll 原生库问题排查
  2. 密码安全: 加盐(Salt) + HMACSHA256 加密
  3. 用户登录功能实现
  4. C# 事件(Event)与委托(Action) - 窗口间通信
  5. AppSession 全局状态管理
  6. MainWindow 底部导航栏设计与 Tag 属性
  7. Window vs UserControl + ContentControl 页面切换架构
  8. 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

排查过程:

  1. runtimes\win-x64\native\ 复制 e_sqlite3.dll,但机器上 AnyCPU 实际运行 x86 进程,加载 x64 DLL 直接崩溃
  2. 改为 runtimes\win-x86\native\ - 可以了
  3. 但放在 runtimes 子目录下 - .NET Framework 4.7.2 找不到
  4. 最终方案 : 直接把 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 打开了,但注销时怎么办?

  • TopBarMainWindow 里面,它不能直接操作 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;
}
  • senderobject 类型 - "谁触发了这个事件"
  • 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

十二、明日计划

  1. 完善 MainWindow 的 Home 页面内容(设备概览仪表盘)
  2. 修复按钮背景色不显示问题(自定义 ControlTemplate)
  3. 手动操作页面 - 基本控件布局
  4. 持续学习 WPF 数据绑定
    "@

System.IO.File::WriteAllText(path, content, $utf8)

Write-Output "Blog written successfully!"

相关推荐
HY小宝F2 小时前
树莓派 FFmpeg 实战笔记(二):命令行、源码编译与硬件编码的真相
学习·职场和发展·ffmpeg
树的枝2 小时前
【STM32】03.中断和EXTI外设
笔记·stm32·单片机·嵌入式硬件
2601_967264282 小时前
Jetpack Compose 实践指南:从入门到进阶
java·学习
Wang's Blog2 小时前
PostgreSQL笔记34:索引优化策略全景解析——从B-tree到HOT的核心原理与实践
数据库·笔记·postgresql
金字塔頂の蝸牛2 小时前
商务日语口语实用手册
笔记·学习笔记·日语·外语
小雪崩3 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
HwJack203 小时前
UIAbility 生命周期全链路:从冷启动到热启动的实战笔记
笔记·华为·harmonyos
九硕智慧建筑一体化厂家3 小时前
直流智能照明|全场景节能升级!打造安全低碳的智慧建筑光环境
运维·笔记·安全·智慧城市
yiqiefeimeng3 小时前
C语言指针难倒90%人?买房比喻让你瞬间开窍
c语言·学习·编程·指针·比喻