C#开发windows程序学习记录

collectcrop Lv3

C#开发学习记录1

开发环境:Visual Studio 2022

前言

从高中时就对开发软件比较感兴趣,用过pygame写过小游戏,当时其实也想用Unity来写个游戏来着。不过高中确实学业压力比较重,加上Unity主要是用C#作为编程语言的,当时也没接触过C#语言,就不了了之。大一又误入安全歧途:smirk:,一直在打ctf,现在趁寒假学点windows程序开发。一开始我看了b站上的视频学习windows程序设计,用的是windows api,不过都是上古(指5年前)的产物了。后面一问gpt才发现,C#是 Windows 应用程序开发的首选语言,尤其是对于快速开发而言。这里就不再从基础语法学起,直接结合ai智慧搓一个小项目来学习吧。

因为我平时做学习的记录都是直接用markdown记录,那么如果想将文章部署到我的博客

这里我们的项目目标是将一般的Markdown文件转换成Hexo特定主题的文章格式,其实hexo是可以直接解析markdown语法,但是需要手动把图片复制到特定目录下,并且图片的显示却会出现问题。看了hexo的开发文档发现可以用{% asset_img img_name %}来指定显示的图片。那么我们可以就以这个目标来学习windows的程序开发。

先安装个AI辅助插件,增加效率。

功能设计

  1. 用户界面 (UI) 设计
  • 功能
    • 提供直观的图形界面,让用户轻松完成文件选择、设置和 Markdown 文件转换。
  • 具体实现
    • 文件选择界面:使用 TreeViewListView 显示文件夹结构,支持拖拽文件。
    • 设置界面:使用 TextBoxComboBox 等控件来输入 Hexo 路径、选择主题、设置发布模式,切换语言等。
    • 进度显示和日志:在转换和处理过程中,显示进度条、日志或消息提示,帮助用户了解当前操作进展。
    • 错误提示:当出现错误时,显示友好的错误消息,帮助用户解决问题。
  1. 设置界面 (Settings Interface)
  • 功能
    • 设置 Hexo 本地路径,让程序知道 Hexo 文件夹的位置。
    • 设置显示字体与大小。
    • 选择 Hexo 主题,或者设置 Hexo 相关配置项。
    • 设置显示语言
  • 具体实现
    • 使用表单控件(例如 TextBoxComboBox 等)来接收用户输入的路径、模式和主题等信息。

    • 使用 FolderBrowserDialog 来允许用户选择本地 Hexo 路径。

    • 提供保存设置的功能,可以将用户选择的路径和设置保存到配置文件中(如 .json.xml 格式)或者注册表中,保证下次启动时能够恢复。

  1. Markdown 转换和图片处理功能 (Markdown Conversion and Image Handling)
  • 功能
    • 将用户选定的 Markdown 文件转为 Hexo 特定主题的文章格式。
    • 自动处理图片,复制到 Hexo 主题的特定目录,并使用 Hexo 的图片语法(例如 {% asset_img img_name %})来引用图片。
  • 具体实现
    • 解析 Markdown 文件内容,是直接使用文件IO操作,增加和替换特定内容。
    • 提取 Markdown 文件中的图片路径并处理:
      • 将图片文件复制到 Hexo 主题目录中的 /source/images/ 或其他指定目录。
      • 在生成的文章中,使用 Hexo 语法 {% asset_img img_name %} 来引用图片。
    • 图片复制时,检查目标目录是否已经有相同的文件,避免覆盖。
    • 可以使用正则表达式或其他文本处理方法来解析和替换 Markdown 中的图片路径。
  1. 插件系统 (Plugin System)
  • 功能
    • 允许未来扩展,比如自动获取 Hexo 特定主题配置、提供更多 Hexo 主题支持、图像优化等。
    • 可以支持插件的安装、卸载和更新。
  • 具体实现
    • 设计插件接口,允许通过 DLL 动态加载插件。
    • 使用 C# 的反射(Reflection)机制动态加载插件并调用其中的功能。
    • 设计一个简单的插件管理器,允许用户选择和管理插件。
  1. 可拓展性和未来计划
  • 功能
    • 未来可以加入更多的 Hexo 特定功能,如一键获取和配置 Hexo 特定主题,自动生成页面摘要、标签等。
    • 也可以扩展支持其他平台的静态网站生成器,或者为用户提供更多的 Markdown 格式转换选项。
  • 具体实现
    • 通过插件架构,将新功能封装为独立的插件,用户可以根据需求安装和启用。
    • 提供 SDK 或文档,允许第三方开发者为项目开发自定义插件。

UI界面设计

菜单栏实现

创建好项目后,将会看到一个默认的窗体(Form)。默认情况下,Visual Studio 会打开窗体的设计视图(设计器视图),可以在这里设计界面。

比如我们要添加按钮组件,就可以在 设计视图 中,你可以通过工具箱拖放控件来创建界面元素。

  1. 打开 工具箱(通常位于 Visual Studio 窗口的左侧,如果没有看到工具箱,可以通过 视图 > 工具箱 打开)。
  2. 工具箱 中,找到 按钮(Button)控件。
  3. 将按钮从工具箱中拖动到窗体上。
  4. 放置按钮后,你可以调整按钮的位置、大小以及其他属性。

之后就会创建button1的组件,我们可以在button1_Click中设计其被点击后的行为。

不过常见的菜单栏组件一般为MenuStrip控件,而不是靠button的堆叠。这里我们的目标是先实现一个菜单栏,里面有打开、设置、插件和帮助四个选项。过程就是从工具箱中选MenuStrip拖到设计图中。我们还可以设置子菜单,但这里我们先从简单的功能实现起,暂时不用子菜单。

接下来我们要实现的是点击 File 菜单后,显示本机目录选择对话框,并将选中的文件目录保存到某个变量。首先我们来看一下InitializeComponent的实现,这里所有菜单组件的实现都类似,所以我们只用看一个File组件对应的实现。实际上,这些代码都是自动生成的,不需要我们更改。但为了更好理解过程,这里进行细致的学习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 
// menuStrip1
//
this.menuStrip1.ImageScalingSize = new System.Drawing.Size(20, 20);
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.FileToolStripMenuItem,
this.SettingsToolStripMenuItem,
this.PluginsToolStripMenuItem,
this.HelpToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Size = new System.Drawing.Size(948, 28);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// FileToolStripMenuItem
//
this.FileToolStripMenuItem.Name = "FileToolStripMenuItem";
this.FileToolStripMenuItem.Size = new System.Drawing.Size(48, 24);
this.FileToolStripMenuItem.Text = "File";
this.FileToolStripMenuItem.Click += new System.EventHandler(this.FileToolStripMenuItem_Click);

menuStrip1:这是菜单栏的容器控件,负责将菜单项(如 File)显示在窗体顶部。

FileToolStripMenuItem:这是菜单栏中的一个菜单项,用来表示 File 菜单。

this.menuStrip1.Items.AddRange(...):将多个组件添加到 menuStrip1 中,表示在菜单栏中添加 FileSettings等菜单项。

this.FileToolStripMenuItem.Click += new System.EventHandler(this.FileToolStripMenuItem_Click);:这行代码表示当点击 File 菜单项时,会触发 FileToolStripMenuItem_Click 方法。

其余一些设置名称和设置大小的内容就不作赘述。

可视化文件选取实现

FileToolStripMenuItem_Click实现如下(在Form1.cs中),是用OpenFileDialog实现可视化菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Markdown2Hexo
{
public partial class Form1 : Form
{
// 用于保存选中的文件夹路径
private string selectedFilePath;
public Form1()
{
InitializeComponent();
}

private void FileToolStripMenuItem_Click(object sender, EventArgs e)
{
// 创建文件选择对话框
using (OpenFileDialog fileDialog = new OpenFileDialog())
{
// 设置对话框的描述
fileDialog.Title = "请选择文件";
fileDialog.Filter = "Markdown 文件 (*.md)|*.md|所有文件 (*.*)|*.*"; // 你可以根据需要设置过滤器

// 显示文件选择对话框并检查用户是否选择了文件
if (fileDialog.ShowDialog() == DialogResult.OK)
{
// 获取选中的文件路径
selectedFilePath = fileDialog.FileName;

// 显示选中文件的路径
MessageBox.Show($"您选择的文件路径是: {selectedFilePath}");

// 你可以在这里处理选中的文件
}
}
}
}
}

获取到了选择的文件后,就可以进行进一步的处理了。为了将业务逻辑和具体的功能处理应该尽量与界面逻辑分离,所以我们要新建一个FileProcessor类,里面创建ProcessMarkdownFile方法来用于实现业务逻辑。而在Form1.cs中可以简单的创建对象并调用其方法。

1
2
3
4
5
6
7
8
9
10
11
//Form1.cs
......
if (fileDialog.ShowDialog() == DialogResult.OK)
{
// 获取选中的文件路径
selectedFilePath = fileDialog.FileName;

// 处理选中的文件
FileProcessor processor = new FileProcessor();
processor.ProcessMarkdownFile(selectedFilePath);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//FileProcessor.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace Markdown2Hexo
{
internal class FileProcessor
{
// 处理Markdown文件
public void ProcessMarkdownFile(string filePath)
{
......
}

}
}
窗口大小获取

获取各组件大小是设计UI的关键内容。

获取整个窗口的大小

1
2
3
4
5
6
7
8
9
// 获取窗口的大小(不包括标题栏和边框)
Size windowSize = this.ClientSize;

// 获取窗口的总大小(包括标题栏和边框)
Size totalWindowSize = this.Size;

// 显示结果
MessageBox.Show($"Window Size: {windowSize.Width} x {windowSize.Height}\n" +
$"Total Window Size: {totalWindowSize.Width} x {totalWindowSize.Height}");

获取 MenuStrip 的大小

1
2
3
4
// 获取 MenuStrip 的大小
Size menuStripSize = menuStrip1.Size;
// 显示结果
MessageBox.Show($"MenuStrip Size: {menuStripSize.Width} x {menuStripSize.Height}");
单窗口页面切换实现

之后我们先不急于实现业务逻辑,继续设计settings的UI。这里我不想弹出一个新的窗体进行窗口配置,目标就是在原先窗口中能有窗口选项卡进行切换。这里可以选择用TabControl组件进行实现。当用户点击 MenuStrip 中的 SettingsHelp 等菜单项时,在 TabControl 中动态创建一个对应的 TabPage 组件。每个TabPage都能够往里面add各种控件,相当于一个我们要设计的页面,而主窗口显示哪个TabPage就是由TabControl进行控制。

先了解下TabControl的常用属性与方法:

常用属性

  1. SelectedTab

    获取或设置当前选中的 TabPage

    类型: TabPage

  2. TabPages

    获取包含所有选项卡的 TabPageCollection,可以通过该属性访问、添加、删除选项卡。

    类型: TabPageCollection

  3. TabCount

    获取 TabControl 中的选项卡数目。

    类型: int

  4. Alignment

    获取或设置选项卡的对齐方式(例如,顶部、底部、左侧或右侧)。

    类型: TabAlignment

    1
    tabControl.Alignment = TabAlignment.Top; // 顶部对齐
  5. ItemSize

    获取或设置选项卡的大小(宽度和高度)。

    类型: Size

    1
    tabControl.ItemSize = new Size(100, 30); // 设置选项卡宽度和高度
  6. Multiline

    获取或设置是否允许选项卡多行显示。

    类型: bool

  7. TabIndex

    获取或设置 TabControl 的索引(即它在窗体中所占的位置)。

    类型: int

    1
    2
    int tabIndex = tabControl.TabIndex;
    tabControl.TabIndex = 1; // 设置选项卡的索引
  8. TabStop

    获取或设置是否允许在 TabControl 中使用 Tab 键进行切换。

    类型: bool

    1
    tabControl.TabStop = false; // 禁用 Tab 键切换
  9. Dock

    获取或设置 TabControl 的停靠方式。

    类型: DockStyle

    1
    tabControl.Dock = DockStyle.Fill; // 填充整个容器
  10. HeightWidth

    获取或设置 TabControl 的高度和宽度。

    类型: int

    1
    2
    tabControl.Height = 300; // 设置高度
    tabControl.Width = 500; // 设置宽度

常用方法

  1. Add(TabPage page)

    TabControl 中添加一个新的 TabPage

    1
    2
    TabPage newPage = new TabPage("New Tab");
    tabControl.TabPages.Add(newPage);
  2. Remove(TabPage page)

    TabControl 中移除一个 TabPage

    1
    tabControl.TabPages.Remove(someTabPage);
  3. Clear()

    清除所有的 TabPage

    1
    tabControl.TabPages.Clear(); // 清空所有选项卡
  4. SelectTab(int index)

    通过索引选择某个 TabPage

    1
    tabControl.SelectTab(1); // 选择第2个选项卡(索引从0开始)
  5. SelectTab(string tabName)

    通过选项卡的名称选择某个 TabPage

    1
    tabControl.SelectTab("TabName"); // 选择名称为 "TabName" 的选项卡
  6. ContainsKey(string key)

    判断某个 TabPage 是否存在,基于其 Name 属性。

    1
    bool exists = tabControl.TabPages.ContainsKey("TabName");
  7. Find(string key, bool searchAllTabs)

    查找具有指定名称的 TabPage,可以指定是否在所有选项卡中查找。

    1
    TabPage foundTab = tabControl.TabPages.Find("TabName", true)[0];
  8. EnableDoubleBuffering()

    设置 TabControl 是否使用双缓冲机制(帮助避免闪烁)。

    1
    tabControl.DoubleBuffered = true;

常用事件

  1. SelectedIndexChanged

    当选中的选项卡发生变化时触发该事件。

    1
    2
    3
    4
    tabControl.SelectedIndexChanged += (s, e) => 
    {
    MessageBox.Show("选项卡已切换");
    };
  2. Selecting

    当选项卡即将被选中时触发该事件。

    1
    2
    3
    4
    tabControl.Selecting += (s, e) =>
    {
    MessageBox.Show("选项卡正在被选择");
    };
  3. Deselected

    当选项卡被取消选中时触发该事件。

    1
    2
    3
    4
    tabControl.Deselected += (s, e) =>
    {
    MessageBox.Show("选项卡已取消选择");
    };
  4. TabIndexChanged

    当选项卡的索引发生变化时触发该事件。

    1
    2
    3
    4
    tabControl.TabIndexChanged += (s, e) =>
    {
    MessageBox.Show("选项卡索引发生变化");
    };

具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//Form1.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Markdown2Hexo
{
public partial class Form1 : Form
{
// 用于保存选中的文件夹路径
private string selectedFilePath;
private TabControl tabControl;
public Form1()
{
InitializeComponent();
InitializeCustomComponents();
}

private void SettingsToolStripMenuItem_Click(object sender, EventArgs e)
{
AddTab("Settings");
}

private void HelpToolStripMenuItem_Click(object sender, EventArgs e)
{
AddTab("Help");
}

// 动态创建一个 TabPage,并添加到 TabControl
private void AddTab(string tabName)
{
// 检查该 TabPage 是否已经存在
if (tabControl.TabPages.ContainsKey(tabName))
{
// 如果已经存在,直接选中它
tabControl.SelectedTab = tabControl.TabPages[tabName];
return;
}

// 创建新的 TabPage
TabPage newTab = new TabPage(tabName)
{
Name = tabName
};

// 给 TabPage 添加内容
Label label = new Label
{
Text = $"{tabName} Content",
Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleCenter
};
newTab.Controls.Add(label);

// 将 TabPage 添加到 TabControl
tabControl.TabPages.Add(newTab);
tabControl.SelectedTab = newTab; // 自动选择新创建的 TabPage
}
1
2
3
4
5
6
7
8
9
//Form1.Designer.cs
...
private void InitializeCustomComponents()
{
// 创建 TabControl
tabControl = new TabControl();
tabControl.Dock = DockStyle.Fill; // 将 TabControl 填充整个窗体
this.Controls.Add(tabControl);
}

此时我们点击Settings就可以更改主窗口的显示了。

设置界面实现

这里由于settings是一块单独的逻辑设置,我们可以将其提取为一个单独的类,并且使用json来管理设置项。那么我们之后在主程序中要用到设置的内容时,只需要示例化设置对象并读取对应属性即可。而且可以在这个类中实现每一条配置的面板(包括标签与输入框或选项框)的返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.IO;
using System.Drawing;
using System.Windows.Forms;

namespace Markdown2Hexo
{
internal class SettingsData
{
public string Language { get; set; }
public string SelectedTheme { get; set; }
public string HexoDirectory { get; set; }
}

internal class Settings
{
// 设置项
public string Language { get; set; } // 语言选项
public string SelectedTheme { get; set; } // 主题选项
public string HexoDirectory { get; set; } // Hexo 目录路径
private static string settingsFilePath = "settings.json";
private SettingsData settingsData;
public ComboBox optionLanguage { get; private set; } // 多选框控件
public TextBox hexoDirectoryTextBox { get; private set; } // 文本框控件
public Button browseButton { get; private set; } // 按钮控件
// 构造函数
public Settings()
{
}

// 加载设置
public void LoadSettings()
{
if (File.Exists(settingsFilePath))
{
string json = File.ReadAllText(settingsFilePath);
var settings = JsonConvert.DeserializeObject<Settings>(json);
this.Language = settings.Language;
this.SelectedTheme = settings.SelectedTheme;
this.HexoDirectory = settings.HexoDirectory;
}
}
// 保存设置
public void SaveSettings()
{
// 创建一个新的对象,只包含需要序列化的设置项
settingsData = new SettingsData
{
Language = this.Language,
SelectedTheme = this.SelectedTheme,
HexoDirectory = this.HexoDirectory
};

string json = JsonConvert.SerializeObject(settingsData, Formatting.Indented);
File.WriteAllText(settingsFilePath, json);
}
}
}

这里我们设置SettingsData这个类的原因在于后面保存设置时,如果直接把Settings这个含有各个控件的类进行序列化,会遇到一个自引用循环。具体来说,ComboBox 控件中的某些属性(例如 AccessibilityObject)可能包含对该控件本身的引用,导致在序列化时发生死循环。所以这里我们把其设置属性全部提取到SettingsData来避免上述错误发生。

需要注意的是,settings.js的输出目录为projectdir/bin/Debug/settings.json,我们如果需要手动赋个初值,也需要在这个目录下创建,而不是直接用visual studio的创建文件功能。

布局设计

由于我们有很多选项需要显示在窗口中,所以我们要利用TableLayoutPanelFlowLayoutPanel等布局控件来优化布局,不然一个一个设置坐标,还要想办法根据屏幕尺寸自适应,会相当麻烦。进阶一点可以使用 GroupBox 或者 TabControl 来进行更细化的分组。这里我们目前需要的选项较少,就先只用TableLayoutPanel进行布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private void AddTab(string tabName)
{
// 检查该 TabPage 是否已经存在
if (tabControl.TabPages.ContainsKey(tabName))
{
// 如果已经存在,直接选中它
tabControl.SelectedTab = tabControl.TabPages[tabName];
return;
}

// 创建新的 TabPage
TabPage newTab = new TabPage(tabName)
{
Name = tabName
};

// 布局设计
TableLayoutPanel tableLayoutPanel = new TableLayoutPanel
{
RowCount = 5, // 设置行数,根据实际需要调整
ColumnCount = 2, // 设置列数,通常是 2 列
Dock = DockStyle.Fill
};

// 根据选中的菜单来切换视图
switch (tabName)
{
case "File":
break;

case "Help":
break;

case "Settings":
Settings settings = new Settings(); //获取设置属性
settings.LoadSettings();


tableLayoutPanel.Controls.Add(settings.CreateLanguageSetting(), 0, 0);
tableLayoutPanel.Controls.Add(settings.CreateHexoDirectorySetting(), 0, 1);

newTab.Controls.Add(tableLayoutPanel);
break;

case "Plugins":
break;
}


// 将 TabPage 添加到 TabControl
tabControl.TabPages.Add(newTab);
tabControl.SelectedTab = newTab; // 自动选择新创建的 TabPage
}

这里的OptionLanguageComboBox方法我们在下文中会给出具体实现。

语言切换功能

目的是能够支持中文和英文两种语言。这里我们打算在 SettingsTabPage 中渲染一个选项条,点击后展示待选项,并且实现鼠标悬停时高亮显示选项。

在Settings.cs中加入如下几个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public Control CreateLanguageSetting()
{
// 创建 TableLayoutPanel
TableLayoutPanel tableLayoutPanel = new TableLayoutPanel
{
ColumnCount = 2, // 创建两列,第一列放 Label,第二列放 ComboBox
RowCount = 1, // 只有一行
Dock = DockStyle.Fill,
AutoSize = true // 自动适应大小
};

// 设置列宽度比例
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20));
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60));

Label label1 = new Label
{
Text = "Language",
TextAlign = ContentAlignment.MiddleLeft, // 设置文字在 Label 中垂直和水平居中
Dock = DockStyle.Fill // 使 Label 填满单元格
};

optionLanguage = new ComboBox
{
Name = "Language", // 设置 Name
DropDownStyle = ComboBoxStyle.DropDownList,
Width = 200,
Location = new Point(30, 50)
};

optionLanguage.Items.Add("zh");
optionLanguage.Items.Add("en");
int index = optionLanguage.Items.IndexOf(Language);
if (index != -1)
optionLanguage.SelectedIndex = index;
else
optionLanguage.SelectedIndex = 0;

optionLanguage.SelectedIndexChanged += ComboBox_SelectedIndexChanged;

tableLayoutPanel.Controls.Add(label1);
tableLayoutPanel.Controls.Add(optionLanguage);
return tableLayoutPanel;
}

// ComboBox 的选项变化事件处理方法
private void ComboBox_SelectedIndexChanged(object sender, EventArgs e)
{
// 将 sender 强制转换为 ComboBox,以便访问其属性
ComboBox comboBox = sender as ComboBox;

if (comboBox != null)
{
// 获取选中的索引
int selectedIndex = comboBox.SelectedIndex;

// 显示相应的多选框面板
ChangeAndSave(selectedIndex, comboBox.Name);
}
}

// 方法:切换选项并保存设置
public void ChangeAndSave(int selectedIndex, string settingType)
{
switch (settingType)
{
case "Language":
// MessageBox.Show($"选项已设置:" + optionLanguage.Items[selectedIndex].ToString());
this.Language = optionLanguage.Items[selectedIndex].ToString();
break;
case "SelectedTheme":
this.SelectedTheme = optionLanguage.Items[selectedIndex].ToString();
break;
}
// 保存到文件
SaveSettings();
}

这里内部也是使用TableLayoutPanel进行布局,可以通过ColumnStyles设置列宽度,从而在一行中对标签以及选项框进行布局

可以看到已经成功的渲染出了选项标签与选择框。

Hexo目录设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public Control CreateHexoDirectorySetting()
{
// 创建 TableLayoutPanel
TableLayoutPanel tableLayoutPanel = new TableLayoutPanel
{
ColumnCount = 3, // 创建三列,第一列放 Label,第二列放 TextBox,第三列放 Button
RowCount = 1, // 只有一行
Dock = DockStyle.Fill,
AutoSize = true // 自动适应大小
};

// 设置列宽度(比例为 2:5:1)
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20)); // 第一列:20%
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 60)); // 第二列:60%
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20)); // 第三列:20%

// 创建提示文字
Label label1 = new Label
{
Text = "Hexo Directory",
TextAlign = ContentAlignment.MiddleLeft, // 使文字左对齐
Dock = DockStyle.Fill // 使 Label 填充其所在的单元格
};

// 创建文本框
hexoDirectoryTextBox = new TextBox
{
Text = this.HexoDirectory, // 显示已有的 Hexo 目录路径
Dock = DockStyle.Fill
};

// 创建浏览按钮
browseButton = new Button
{
Text = "Browse",
Dock = DockStyle.Fill
};

// 浏览按钮点击事件
browseButton.Click += BrowseButton_Click;

// 将控件添加到 TableLayoutPanel 中
tableLayoutPanel.Controls.Add(label1, 0, 0); // 添加 Label 到第一列
tableLayoutPanel.Controls.Add(hexoDirectoryTextBox, 1, 0); // 添加 TextBox 到第二列
tableLayoutPanel.Controls.Add(browseButton, 2, 0); // 添加 Button 到第三列

return tableLayoutPanel;
}

// 浏览按钮点击事件处理
private void BrowseButton_Click(object sender, EventArgs e)
{
using (FolderBrowserDialog folderDialog = new FolderBrowserDialog())
{
// 设置初始文件夹路径为当前路径
folderDialog.SelectedPath = hexoDirectoryTextBox.Text;

// 弹出文件夹选择框
if (folderDialog.ShowDialog() == DialogResult.OK)
{
// 设置 TextBox 为选择的文件夹路径
MessageBox.Show("Hexo directory has been set to " + folderDialog.SelectedPath);
hexoDirectoryTextBox.Text = folderDialog.SelectedPath;
HexoDirectory = folderDialog.SelectedPath;
}
SaveSettings();
}
}

// 检查用户输入的路径是否有效
public bool ValidateHexoDirectory()
{
string path = hexoDirectoryTextBox.Text;
if (Directory.Exists(path))
{
this.HexoDirectory = path;
return true; // 路径有效
}
else
{
MessageBox.Show("Invalid Hexo directory. Please select a valid path.");
return false; // 路径无效
}
}

选文件路径是用folderDialog,类似于之前可视化文件选取所用到的OpenFileDialog。这里我们也是返回一个tableLayoutPanel到主程序,然后再在主程序中将其增加到对应的设计面板tableLayoutPanel中,最后主程序把设计面板加入SettingsTabPage即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
......
case "Settings":
Settings settings = new Settings(); //获取设置属性
settings.LoadSettings();


tableLayoutPanel.Controls.Add(settings.CreateLanguageSetting(), 0, 0);
tableLayoutPanel.Controls.Add(settings.CreateHexoDirectorySetting(), 0, 1);

newTab.Controls.Add(tableLayoutPanel);

break;
......

文件格式转换功能实现

前面我们已经解决了获取输入文件路径以及输出目录的问题,然后我们就可以着手实现我们的核心功能,也就是图片文件转移与输出目标格式文件。这里我们还可以可视化实现hexo文章标签与类别的设置。以下是整个FileProcessor.cs的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Windows.Forms;
using System.Drawing;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace Markdown2Hexo
{
internal class FileProcessor
{
public string Tag { get; set; } // Hexo文章标签
public string Title { get; set; } // Hexo文章标题
public string Category { get; set; } // Hexo文章分类
private FlowLayoutPanel tagsPanel;
private Button addTagButton;
private Button removeTagButton;
private List<TextBox> tagsTextBoxes = new List<TextBox>();
private FlowLayoutPanel categoriesPanel;
private Button addCategoryButton;
private Button removeCategoryButton;
private List<TextBox> categoriesTextBoxes = new List<TextBox>();

private RichTextBox processInfoBox;

//渲染Process Markdown面板
public Control CreateProcessMarkdownPanel(string filePath)
{
// 创建 TableLayoutPanel 用于布局
TableLayoutPanel tableLayoutPanel = new TableLayoutPanel
{
RowCount = 6, // 根据需要的行数调整
ColumnCount = 2,
Dock = DockStyle.Fill,
AutoSize = true,
Padding = new Padding(20)
};

tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20));
tableLayoutPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 80));

// 创建文件路径标签
Label filePathLabel = new Label
{
Text = "Source File Path: ",
TextAlign = ContentAlignment.MiddleLeft
};

Label filePathValueLabel = new Label
{
Text = filePath,
TextAlign = ContentAlignment.MiddleLeft,
Font = new Font("Consolas", 10),
AutoSize = false
};

// 计算文本的宽度
SizeF textSize = TextRenderer.MeasureText(filePath, filePathValueLabel.Font);
filePathValueLabel.Width = (int)textSize.Width + 20; // 20 是为了留一些额外的空间,可以根据需要调整

// 创建标题标签
Label titleLabel = new Label
{
Text = "Title",
TextAlign = ContentAlignment.MiddleLeft
};

// 创建标题输入框
TextBox titleTextBox = new TextBox
{
Multiline = true,
Width = 300
};

// 创建标签输入框
Label tagsLabel = new Label
{
Text = "Tags",
TextAlign = ContentAlignment.MiddleLeft
};

// 创建 FlowLayoutPanel 用来容纳多个输入框
tagsPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight, // 横向排列
WrapContents = true, // 容量满了之后自动换行
Padding = new Padding(10)
};
// 创建加号按钮
addTagButton = new Button
{
Text = "+",
AutoSize = true
};
addTagButton.Click += AddTagButton_Click;

// 创建减号按钮
removeTagButton = new Button
{
Text = "-",
AutoSize = true
};
removeTagButton.Click += RemoveTagButton_Click;
// 添加初始的标签输入框
AddInputBox(tagsPanel, tagsTextBoxes, addTagButton, removeTagButton);



// 创建分类输入框
Label categoryLabel = new Label
{
Text = "Category",
TextAlign = ContentAlignment.MiddleLeft
};

// 创建 FlowLayoutPanel 用来容纳多个输入框
categoriesPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight, // 横向排列
WrapContents = true, // 容量满了之后自动换行
Padding = new Padding(10)
};
// 创建加号按钮
addCategoryButton = new Button
{
Text = "+",
AutoSize = true
};
addCategoryButton.Click += AddCategoryButton_Click;

// 创建减号按钮
removeCategoryButton = new Button
{
Text = "-",
AutoSize = true
};
removeCategoryButton.Click += RemoveCategoryButton_Click;
// 添加初始的标签输入框
AddInputBox(categoriesPanel, categoriesTextBoxes, addCategoryButton, removeCategoryButton);

// 创建确定按钮
Button confirmButton = new Button
{
Text = "Process",
Width = 100,
Height = 40
};

// 点击按钮时执行 ProcessMarkdownFile
confirmButton.Click += (sender, e) =>
{
confirmButton.Visible = false; // 隐藏确定按钮,防止用户多次点击
string title = titleTextBox.Text;
List<string> tags = GetTags();
List<string> category = GetCategories();

// 非空检测
if (string.IsNullOrWhiteSpace(title))
{
MessageBox.Show("Title cannot be empty.", "Input Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return; // 直接退出,不继续执行后续代码
}
ProcessMarkdownFile(title, tags, category, filePath);
};

// 创建 Process 信息框
processInfoBox = new RichTextBox
{
Multiline = true,
Width = 600,
Height = 200,
ReadOnly = true, // 设置为只读
ScrollBars = RichTextBoxScrollBars.Vertical, // 启用滚动条
WordWrap = true, // 自动换行
Font = new Font("Consolas", 12)
};

// 将控件添加到 TableLayoutPanel
tableLayoutPanel.Controls.Add(filePathLabel, 0, 0);
tableLayoutPanel.Controls.Add(filePathValueLabel, 1, 0);
tableLayoutPanel.Controls.Add(titleLabel, 0, 1);
tableLayoutPanel.Controls.Add(titleTextBox, 1, 1);
tableLayoutPanel.Controls.Add(tagsLabel, 0, 2);
tableLayoutPanel.Controls.Add(tagsPanel, 1, 2);
tableLayoutPanel.Controls.Add(categoryLabel, 0, 3);
tableLayoutPanel.Controls.Add(categoriesPanel, 1, 3);
tableLayoutPanel.Controls.Add(confirmButton, 1, 4);
tableLayoutPanel.Controls.Add(processInfoBox, 1, 5);

return tableLayoutPanel;
}

private void AddInputBox(Control inputPanel, List<TextBox> textBoxesList, Button addButton, Button removeButton)
{
// 创建新的 TextBox 控件
TextBox newInputTextBox = new TextBox
{
Width = 100, // 设置适当的宽度
Margin = new Padding(5) // 设置间距
};

// 将新的 TextBox 添加到列表和面板
textBoxesList.Add(newInputTextBox);

// 先移除+-号按钮,之后再添加,确保在最后一个位置
if (inputPanel.Controls.Contains(removeButton))
inputPanel.Controls.Remove(removeButton);
inputPanel.Controls.Remove(addButton);

// 添加新的输入框和按钮
inputPanel.Controls.Add(newInputTextBox);
inputPanel.Controls.Add(addButton); // 将加号按钮添加到 inputPanel 中
if (textBoxesList.Count > 1)
inputPanel.Controls.Add(removeButton); // 将减号按钮添加到 inputPanel 中
}

private void RemoveInputBox(Control inputPanel, List<TextBox> textBoxesList, Button removeButton)
{
if (textBoxesList.Count > 1)
{
// 获取列表中最后一个添加的 TextBox 和相应的按钮
TextBox lastInputTextBox = textBoxesList.Last();

// 从 inputPanel 和 textBoxesList 中移除最后一个 TextBox 和按钮
if (lastInputTextBox != null)
{
inputPanel.Controls.Remove(lastInputTextBox);
textBoxesList.Remove(lastInputTextBox);
}
}

// 如果只有一个标签输入框,则隐藏减号按钮
if (textBoxesList.Count == 1)
{
if (inputPanel.Controls.Contains(removeButton))
inputPanel.Controls.Remove(removeButton);
}
}

private void AddTagButton_Click(object sender, EventArgs e)
{
AddInputBox(tagsPanel, tagsTextBoxes, addTagButton, removeTagButton);
}

private void RemoveTagButton_Click(object sender, EventArgs e)
{
RemoveInputBox(tagsPanel, tagsTextBoxes, removeTagButton);
}

private void AddCategoryButton_Click(object sender, EventArgs e)
{
AddInputBox(categoriesPanel, categoriesTextBoxes, addCategoryButton, removeCategoryButton);
}

private void RemoveCategoryButton_Click(object sender, EventArgs e)
{
RemoveInputBox(categoriesPanel, categoriesTextBoxes, removeCategoryButton);
}
// 获取用户输入的所有标签
public List<string> GetTags()
{
List<string> tags = new List<string>();
foreach (var textBox in tagsTextBoxes)
{
if (!string.IsNullOrWhiteSpace(textBox.Text))
{
tags.Add(textBox.Text);
}
}
return tags;
}

// 获取用户输入的所有类型
public List<string> GetCategories()
{
List<string> categories = new List<string>();
foreach (var textBox in categoriesTextBoxes)
{
if (!string.IsNullOrWhiteSpace(textBox.Text))
{
categories.Add(textBox.Text);
}
}
return categories;
}

// 显示提示信息
void AppendMessage(string message, Color color)
{
// 设置不同颜色(可选)
processInfoBox.SelectionColor = color; // 设置文本颜色
processInfoBox.AppendText(message + Environment.NewLine);
processInfoBox.ScrollToCaret(); // 确保显示滚动到文本底部
}


// 处理Markdown文件
public async void ProcessMarkdownFile(string title, List<string> tags, List<string> category, string filePath)
{
Settings settings = new Settings();
settings.LoadSettings();
string hexoDirectory = settings.HexoDirectory;
string rootDirectory = hexoDirectory.Substring(0, hexoDirectory.LastIndexOf("\\"));
string postDirectory = Path.Combine(hexoDirectory, "source", "_posts");
string hexoNewCmd = $"hexo new \"{title}\""; // 创建新文章命令
AppendMessage($"[*] executing command: {hexoNewCmd}", Color.Black);
try
{
// 使用 Process 执行命令
ProcessStartInfo processStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {rootDirectory} && cd {hexoDirectory} && {hexoNewCmd}",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,

};

using (Process process = Process.Start(processStartInfo))
{
string output = await process.StandardOutput.ReadToEndAsync();
string error = await process.StandardError.ReadToEndAsync();

process.WaitForExit();

if (process.ExitCode != 0)
{
MessageBox.Show($"Error executing hexo new: {error}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
AppendMessage($"[-] Error executing hexo new: {error}", Color.Red);
return;
}
if (File.Exists(Path.Combine(postDirectory, title + ".md"))) // 检查新文章是否创建成功
AppendMessage("[+] successfully created new post!",Color.Green);
}
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}

// 获取生成的 Markdown 文件路径
string targetMarkdownFilePath = Path.Combine(hexoDirectory, "source", "_posts", $"{title}.md");
string targetMarkdownPictureDirectory = Path.Combine(hexoDirectory, "source", "_posts", $"{title}");
string fileContent = await Task.Run(() => File.ReadAllText(filePath, Encoding.UTF8)); // 异步读取 Markdown 文件内容

// 构建 YAML 前言部分
StringBuilder frontMatter = new StringBuilder();
frontMatter.AppendLine("---");
frontMatter.AppendLine($"title: {title}");
frontMatter.AppendLine($"date: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");

// 添加 tags
if (tags != null && tags.Count > 0)
{
frontMatter.AppendLine("tags:");
foreach (string tag in tags)
{
frontMatter.AppendLine($" - {tag}");
}
}

// 添加 categories
if (category != null && category.Count > 0)
{
frontMatter.AppendLine("categories:");
foreach (string cat in category)
{
frontMatter.AppendLine($" - {cat}");
}
}

frontMatter.AppendLine("---");

// 正则表达式:查找所有的图片引用
string imagePattern = @"!\[([^\]]+)\]\(([^)]+)\)";
MatchCollection matches = Regex.Matches(fileContent, imagePattern);

foreach (Match match in matches)
{
string altText = match.Groups[1].Value; // 图片描述
string imgPath = match.Groups[2].Value; // 图片路径

imgPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(filePath), imgPath)); // 处理相对路径
imgPath = imgPath.Replace("\\", "/"); // 确保路径使用正斜杠
// 提取图片文件名
string imgFilename = Path.GetFileName(imgPath);

// 检查图片文件是否存在
if (File.Exists(imgPath))
{
try
{
// 复制图片到目标目录
string targetPath = targetMarkdownPictureDirectory+"/" + imgFilename;
await Task.Run(() => File.Copy(imgPath, targetPath, true)); // 异步复制图片

AppendMessage($"[*] Copying image: {imgPath} to {targetPath}", Color.Blue);

// 更新 Markdown 内容
string newReference = $"{{% asset_img {imgFilename} %}}";
fileContent = fileContent.Replace(match.Value, newReference);
}
catch (Exception ex)
{
AppendMessage($"[-] Error moving image {imgPath}: {ex.Message}", Color.Red);
}
}
else
{
AppendMessage($"[-] Image not found: {imgPath}", Color.Red);
// 可以选择是否退出或继续处理
// exit(); // 如果需要退出的话
}
}
// 将内容合并
string finalContent = frontMatter.ToString() + Environment.NewLine + fileContent;
// 写入文件
//File.WriteAllText(targetMarkdownFilePath, finalContent, Encoding.UTF8);
await Task.Run(() => File.WriteAllText(targetMarkdownFilePath, finalContent, Encoding.UTF8));
AppendMessage($"[+] successfully processed {filePath} to {targetMarkdownFilePath}", Color.Green);



}

}
}

获取tags、categories等相关信息UI

主要也是用TableLayoutPanel进行页面设计,这里我们先改一下Form1.cs,让点击File的菜单栏后不是只有弹文件选择,而是先利用之前实现的AddTab来创建一个TabPage,然后再调用FileProcessor.cs中的CreateProcessMarkdownPanel方法来返回文件处理的面板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//Form1.cs
private void FileToolStripMenuItem_Click(object sender, EventArgs e)
{
AddTab("File");
}

switch (tabName)
{
case "File":
// 创建文件选择对话框
using (OpenFileDialog fileDialog = new OpenFileDialog())
{
// 设置对话框的描述
fileDialog.Title = "请选择文件";
fileDialog.Filter = "Markdown 文件 (*.md)|*.md|所有文件 (*.*)|*.*"; // 你可以根据需要设置过滤器

// 显示文件选择对话框并检查用户是否选择了文件
if (fileDialog.ShowDialog() == DialogResult.OK)
{
// 获取选中的文件路径
selectedFilePath = fileDialog.FileName;

// 处理选中的文件
FileProcessor processor = new FileProcessor();
newTab.Controls.Add(processor.CreateProcessMarkdownPanel(selectedFilePath));

}
}
break;
}

每一行的设计也有区别,首先文章标题的输入可以直接使用TextBox文本框获取。而tags和categories由于可以存在多个,可行的方案之一是也用TextBox读取,然后指定某个分隔符来分开多个不同的tag和category。但这样显然比较麻烦,所以我们可以使用FlowLayoutPanel来进行动态的布局设计,目标是可以创建一系列不同的TextBox输入框,点击+按钮新建一个tag输入框,点击-按钮删除一个tag输入框。该布局模板可以动态的根据屏幕大小进行适应,比如可以自动换行,相当方便。+-按钮都要绑定一个点击事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 创建 FlowLayoutPanel 用来容纳多个输入框
tagsPanel = new FlowLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
FlowDirection = FlowDirection.LeftToRight, // 横向排列
WrapContents = true, // 容量满了之后自动换行
Padding = new Padding(10)
};
// 创建加号按钮
addTagButton = new Button
{
Text = "+",
AutoSize = true
};
addTagButton.Click += AddTagButton_Click;

// 创建减号按钮
removeTagButton = new Button
{
Text = "-",
AutoSize = true
};
removeTagButton.Click += RemoveTagButton_Click;
// 添加初始的标签输入框
AddInputBox(tagsPanel, tagsTextBoxes, addTagButton, removeTagButton);
........................................................

private void AddTagButton_Click(object sender, EventArgs e)
{
AddInputBox(tagsPanel, tagsTextBoxes, addTagButton, removeTagButton);
}

private void RemoveTagButton_Click(object sender, EventArgs e)
{
RemoveInputBox(tagsPanel, tagsTextBoxes, removeTagButton);
}

然后就是核心的AddInputBoxRemoveInputBox函数实现。需要注意的是边界条件的处理,我们在只有一个输入框时可以不显示-按钮,以防止越界。而且如果直接往inputPanel(也就是调用者的tagsPanel)中加TextBox,会导致+-不显示在所有输入框之后,比较不美观。所以我们可以每次增加输入框时,先删去原来的+-按钮,待TextBox增加入控件后,最后再次添加按钮回来。这样就能确保按钮位置正确。这里抽象出了AddInputBoxRemoveInputBox函数是为了tag和category的处理都能够复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void AddInputBox(Control inputPanel, List<TextBox> textBoxesList, Button addButton, Button removeButton)
{
// 创建新的 TextBox 控件
TextBox newInputTextBox = new TextBox
{
Width = 100, // 设置适当的宽度
Margin = new Padding(5) // 设置间距
};

// 将新的 TextBox 添加到列表和面板
textBoxesList.Add(newInputTextBox);

// 先移除+-号按钮,之后再添加,确保在最后一个位置
if (inputPanel.Controls.Contains(removeButton))
inputPanel.Controls.Remove(removeButton);
inputPanel.Controls.Remove(addButton);

// 添加新的输入框和按钮
inputPanel.Controls.Add(newInputTextBox);
inputPanel.Controls.Add(addButton); // 将加号按钮添加到 inputPanel 中
if (textBoxesList.Count > 1)
inputPanel.Controls.Add(removeButton); // 将减号按钮添加到 inputPanel 中
}

private void RemoveInputBox(Control inputPanel, List<TextBox> textBoxesList, Button removeButton)
{
if (textBoxesList.Count > 1)
{
// 获取列表中最后一个添加的 TextBox 和相应的按钮
TextBox lastInputTextBox = textBoxesList.Last();

// 从 inputPanel 和 textBoxesList 中移除最后一个 TextBox 和按钮
if (lastInputTextBox != null)
{
inputPanel.Controls.Remove(lastInputTextBox);
textBoxesList.Remove(lastInputTextBox);
}
}

// 如果只有一个标签输入框,则隐藏减号按钮
if (textBoxesList.Count == 1)
{
if (inputPanel.Controls.Contains(removeButton))
inputPanel.Controls.Remove(removeButton);
}

获取用户输入的Tags和Category也比较方便。能够直接返回一个string类型的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取用户输入的所有标签
public List<string> GetTags()
{
List<string> tags = new List<string>();
foreach (var textBox in tagsTextBoxes)
{
if (!string.IsNullOrWhiteSpace(textBox.Text))
{
tags.Add(textBox.Text);
}
}
return tags;
}

然后可以添加一个提交按钮,来触发核心的文件处理函数。按钮点击后就可以隐藏,避免多次点击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建确定按钮
Button confirmButton = new Button
{
Text = "Process",
Width = 100,
Height = 40
};

// 点击按钮时执行 ProcessMarkdownFile
confirmButton.Click += (sender, e) =>
{
confirmButton.Visible = false; // 隐藏确定按钮,防止用户多次点击
string title = titleTextBox.Text;
List<string> tags = GetTags();
List<string> category = GetCategories();

// 非空检测
if (string.IsNullOrWhiteSpace(title))
{
MessageBox.Show("Title cannot be empty.", "Input Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return; // 直接退出,不继续执行后续代码
}
ProcessMarkdownFile(title, tags, category, filePath);
};
消息窗口实现

实现了上述的UI以后,界面还是有点空旷,那么其实我们可以把一些执行中的过程信息显示出来,来丰富界面,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建 Process 信息框
processInfoBox = new RichTextBox
{
Multiline = true,
Width = 600,
Height = 200,
ReadOnly = true, // 设置为只读
ScrollBars = RichTextBoxScrollBars.Vertical, // 启用滚动条
WordWrap = true, // 自动换行
Font = new Font("Consolas", 12)
};
.......................

void AppendMessage(string message, Color color)
{
// 设置不同颜色(可选)
processInfoBox.SelectionColor = color; // 设置文本颜色
processInfoBox.AppendText(message + Environment.NewLine);
processInfoBox.ScrollToCaret(); // 确保显示滚动到文本底部
}

之后我们调用AppendMessage就可以直接把消息显示到屏幕上了。

文件具体处理实现

首先是要利用hexo的new命令来新建一个文章的架构,比较麻烦的是切换到Hexo的目录。首先我们要先从设置中读取出来,然后得截取出盘符,因为在windows上不能直接跨盘符用cd切换目录。最后才能执行hexo new title。这里用异步函数是为了在消息框显示内容更有层次感。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public async void ProcessMarkdownFile(string title, List<string> tags, List<string> category, string filePath)
{
Settings settings = new Settings();
settings.LoadSettings();
string hexoDirectory = settings.HexoDirectory;
string rootDirectory = hexoDirectory.Substring(0, hexoDirectory.LastIndexOf("\\"));
string postDirectory = Path.Combine(hexoDirectory, "source", "_posts");
string hexoNewCmd = $"hexo new \"{title}\""; // 创建新文章命令
AppendMessage($"[*] executing command: {hexoNewCmd}", Color.Black);
try
{
// 使用 Process 执行命令
ProcessStartInfo processStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c {rootDirectory} && cd {hexoDirectory} && {hexoNewCmd}",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,

};

using (Process process = Process.Start(processStartInfo))
{
string output = await process.StandardOutput.ReadToEndAsync();
string error = await process.StandardError.ReadToEndAsync();

process.WaitForExit();

if (process.ExitCode != 0)
{
MessageBox.Show($"Error executing hexo new: {error}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
AppendMessage($"[-] Error executing hexo new: {error}", Color.Red);
return;
}
if (File.Exists(Path.Combine(postDirectory, title + ".md"))) // 检查新文章是否创建成功
AppendMessage("[+] successfully created new post!",Color.Green);
}
}
catch (Exception ex)
{
MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}

然后是根据提供的几个参数来构建hexo的文章的YAML前言部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 构建 YAML 前言部分
StringBuilder frontMatter = new StringBuilder();
frontMatter.AppendLine("---");
frontMatter.AppendLine($"title: {title}");
frontMatter.AppendLine($"date: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");

// 添加 tags
if (tags != null && tags.Count > 0)
{
frontMatter.AppendLine("tags:");
foreach (string tag in tags)
{
frontMatter.AppendLine($" - {tag}");
}
}

// 添加 categories
if (category != null && category.Count > 0)
{
frontMatter.AppendLine("categories:");
foreach (string cat in category)
{
frontMatter.AppendLine($" - {cat}");
}
}

frontMatter.AppendLine("---");

最后就是将所有markdown文件中出现的图片通过正则匹配提取路径,并复制到指定目录,然后将图片语法替换成hexo能够解析的语法。最后将内容合并,写回hexo文章的markdown文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 正则表达式:查找所有的图片引用
string imagePattern = @"!\[([^\]]+)\]\(([^)]+)\)";
MatchCollection matches = Regex.Matches(fileContent, imagePattern);

foreach (Match match in matches)
{
string altText = match.Groups[1].Value; // 图片描述
string imgPath = match.Groups[2].Value; // 图片路径

imgPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(filePath), imgPath)); // 处理相对路径
imgPath = imgPath.Replace("\\", "/"); // 确保路径使用正斜杠
// 提取图片文件名
string imgFilename = Path.GetFileName(imgPath);

// 检查图片文件是否存在
if (File.Exists(imgPath))
{
try
{
// 复制图片到目标目录
string targetPath = targetMarkdownPictureDirectory+"/" + imgFilename;
await Task.Run(() => File.Copy(imgPath, targetPath, true)); // 异步复制图片

AppendMessage($"[*] Copying image: {imgPath} to {targetPath}", Color.Blue);

// 更新 Markdown 内容
string newReference = $"{{% asset_img {imgFilename} %}}";
fileContent = fileContent.Replace(match.Value, newReference);
}
catch (Exception ex)
{
AppendMessage($"[-] Error moving image {imgPath}: {ex.Message}", Color.Red);
}
}
else
{
AppendMessage($"[-] Image not found: {imgPath}", Color.Red);
// 可以选择是否退出或继续处理
// exit(); // 如果需要退出的话
}
}
// 将内容合并
string finalContent = frontMatter.ToString() + Environment.NewLine + fileContent;
// 写入文件
//File.WriteAllText(targetMarkdownFilePath, finalContent, Encoding.UTF8);
await Task.Run(() => File.WriteAllText(targetMarkdownFilePath, finalContent, Encoding.UTF8));
AppendMessage($"[+] successfully processed {filePath} to {targetMarkdownFilePath}", Color.Green);

这样以后,基本功能就差不多实现了,这里只对redefine这个主题进行了适配,别的Hexo主题可能需要调整细节。更多的设置功能也可以后续慢慢的添加进去。

常用控件

控件 常用属性 常用方法
Label - Text:显示的文本
- TextAlign:文本对齐方式
- Font:字体
- ForeColor:文本颜色
- BackColor:背景颜色
- AutoSize:是否自动调整大小
- SetBounds:设置控件的位置和大小
- Focus:设置焦点
Button - Text:按钮文本
- BackColor:按钮背景色
- ForeColor:按钮文字颜色
- Enabled:是否启用按钮
- Size:按钮大小
- Tag:关联的控件
- PerformClick():模拟点击按钮
- Click:按钮点击事件
TextBox - Text:输入文本
- TextAlign:文本对齐方式
- Multiline:是否多行文本框
- MaxLength:最大字符长度
- PasswordChar:密码字符
- Clear():清除文本
- Focus():设置焦点
- SelectAll():全选文本
- Paste():粘贴
ComboBox - Items:项集合
- SelectedItem:选中的项
- SelectedIndex:选中的索引
- DropDownStyle:下拉框风格(如 DropDownList
- Add():添加项
- Remove():移除项
- SelectedIndexChanged:选项变化事件
ListBox - Items:项集合
- SelectedItem:选中的项
- SelectedIndex:选中的索引
- MultiColumn:是否多列显示
- Add():添加项
- Remove():移除项
- Clear():清空项
- SelectedIndexChanged:选项变化事件
CheckBox - Checked:是否选中
- Text:文本
- AutoSize:是否自动调整大小
- CheckState:勾选状态(CheckedUnchecked
- PerformClick():模拟点击
- CheckedChanged:选中状态变化事件
RadioButton - Checked:是否选中
- Text:文本
- GroupName:分组名称,防止多个单选框互相独立
- CheckedChanged:选中状态变化事件
- PerformClick():模拟点击
Panel - BackColor:背景色
- Dock:停靠方式(如 DockStyle.Fill
- Padding:控件的内边距
- Controls.Add():向面板添加控件
- Controls.Remove():移除控件
PictureBox - Image:显示的图片
- SizeMode:图片显示方式(如 StretchImageZoom
- BackColor:背景颜色
- Load():加载图片
- Refresh():重绘图片
RichTextBox - Text:文本内容
- Font:字体
- SelectionStart:选中文本的起始位置
- SelectionLength:选中文本的长度
- AppendText():追加文本
- Clear():清空文本
- Find():查找文本
DateTimePicker - Value:选定的日期时间
- Format:日期时间格式
- MinDate:最小日期
- MaxDate:最大日期
- CloseUp():关闭下拉日历
- DropDown():展开下拉日历
TrackBar - Minimum:最小值
- Maximum:最大值
- Value:当前值
- TickFrequency:刻度频率
- ValueChanged:值变化事件
TabControl - TabPages:Tab页集合
- SelectedTab:当前选中的Tab页
- Alignment:Tab的对齐方式
- SelectTab():切换选中的Tab
ToolTip - ToolTipTitle:工具提示标题
- IsBalloon:是否气泡提示
- InitialDelay:首次显示延迟
- SetToolTip():为控件设置工具提示
TreeView - Nodes:树节点集合
- SelectedNode:选中的节点
- CheckBoxes:是否启用复选框
- ExpandAll():展开所有节点
- CollapseAll():折叠所有节点
DateTimePicker - Value:当前选中的日期时间
- Format:日期时间显示格式
- CloseUp():关闭日历窗口
- DropDown():打开日历窗口
  • 标题: C#开发windows程序学习记录
  • 作者: collectcrop
  • 创建于 : 2025-01-28 00:45:46
  • 更新于 : 2025-01-28 00:45:46
  • 链接: https://collectcrop.github.io/2025/01/28/C-开发windows程序学习记录/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。