[{"content":"Ai太强了，很多常用的创意软件都有了复刻版\n而且都是净室开发，不太有侵权的可能\n我使用ArtCraft的FigCraft的时候，发现代码中有mcp相关的痕迹\n然后稍微看了看代码，发现其实已经完整实现了mcp的功能\n但软件的界面中没提，Github和网页也没讲怎么用\n软件本身实际上分为figcraft和figcraft-cli\n前者是图形化的软件，也就是给人用的\n后者是一个命令行工具，人也能用，但显然给Ai用更方便\n这边使用scoop安装，这样可以自动配置环境变量，用artcraft-launcher要手动配置\nscoop install figcraft 对于mcp的情况，建议采用附加模式启动\n首先需要传入命令行参数启动figcraft\nfigcraft --control 23333 这里的23333是指定的端口，随便填一个就可以\n传命令行参数可以通过创建一个快捷方式传入\n然后配置Agent连接到mcp服务器，原始启动命令是\nfigcraft-cli mcp --attach 23333 保持端口号一致即可\n不同的Agent需要的配置文件会不一样，最好是把命令给Ai，让它自己配置\n然后就可以使用了，Agent和用户可以协同工作，非常方便\n","date":"2026-11-15T17:08:48+08:00","permalink":"/post/articles/2026/%E8%AE%A9agent%E6%8E%A7%E5%88%B6figcraft/","title":"让Agent控制FigCraft"},{"content":"F-Droid 首先配置tuna源\nhttps://mirrors.tuna.tsinghua.edu.cn/fdroid/repo/?fingerprint=43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB Binary Eye GitSync KeePassVault ","date":"2026-10-30T02:29:35+08:00","permalink":"/post/categories/backup/%E9%85%8D%E7%BD%AEandroid/","title":"配置Android"},{"content":"之前提到，通过GDExtension实现Godot插件可以真正把类注册到ClassDB中\n这样就可以让Godot的类型系统区分不同的类型，但这样还不够\n还需要一些额外的知识才可以让实现的类型尽可能接近原生类型\n\u0026lt;0x01\u0026gt; 一个小实验 这里使用godot-rust来构建GDExtension\n扩展的测试代码\n#[derive(GodotClass)] #[class(init, base=Node)] struct TestNode{} #[godot_api] impl INode for TestNode{ fn _ready(){ godot_print!(\u0026#34;TestNode Ready\u0026#34;); } } 构建后，在Godot中创建一个测试场景，添加TestNode\n在TestNode中绑定测试脚本代码\nextend TestNode func _ready() -\u0026gt; void: print(\u0026#34;Script Ready\u0026#34;); 运行后发现，只有Script Ready输出，没有TestNode Ready的输出\n也就是说脚本的_ready实现会覆盖GDExtension的_ready实现\n这就不是很妙了，再怎么说自己实现的节点一定是要基于生命周期写自己的实现的\n如果用户需要对扩展的节点的_ready进行扩展且不影响原本逻辑，那就需要手动调用基类的_ready\n也就是要这样写\nextend TestNode func _ready() -\u0026gt; void: super._ready(); print(\u0026#34;Script Ready\u0026#34;); 但这样写用户必须这样显式指出，容易遗忘而造成一些非预期的运行结果\n并且对于插件来说，节点的逻辑应当是确定的，应该不需要用户额外的控制\n\u0026lt;0x02\u0026gt; 通过notification实现生命周期逻辑 那么问题来了，显然一些原生类型也是要基于生命周期实现自己的逻辑的\n为什么原生类型的节点脚本不需要显式调用基类的_ready这样的方法呢\n难道是godot对原生类型有什么独特的调用吗\n经过我对源代码与文档的探索，可以说对，也不对\n首先，为什么脚本的_ready会覆盖节点实现的_ready\n在Godot中，_ready、_process这样的函数都是虚函数，派生类可以有自己的实现\n也即这个函数的调用点是可变的，可随程序运行更改\nGodot也是通过这样的机制实现了节点可通过脚本来重新定义这些函数的功能\n但这样的修改是会覆盖原来的实现的\n要解决这个问题，就必须要有另一种调用方式，这就要靠notificatioin机制了\n_ready、_process这样的函数本身也表示一个事件\nGodot会在这些事件触发后，一定会通过一个指定的函数做回调，这个函数是_notification\n当然，这个_notification也是虚函数，如果脚本需要修改也是可以的，但一般不会动这个\n查看Control类型的源代码\n可以发现对于原生类型，生命周期逻辑是写在_notification函数里的\n而_ready、_process这样的函数是空出来没实现的\n所以给原生类型写脚本是不需要显式调用基类方法，通过notification机制处理\n如果自己的扩展节点实现了这样的机制，也可以做到这样的效果\nnotification在文档中有提到，但主要就在最佳实践中提到（Godot通知）\n可以说第一次见确实不知道这个是什么意思，名字很难联想到可以与扩展有关 毕竟这个特性写脚本是不需要的，一般人也不会上来就开始写扩展\n\u0026lt;0x03\u0026gt; 如何通过notification实现 首先观察notification的函数签名，这里用godot-rust的api举例\nfn on_notification(\u0026amp;mut self, what: ControlNotification) { /*...*/ } 前面的self不用管，这是rust表示调用者自己的写法\n后面的what是一个ControlNotification类型的参数\n这个what的类型按不同的节点是不同的，这是是Control节点的on_notification\n如果是Object类型，那么what就是ObjectNotification\n无论是什么类型，其实都是一个枚举类型\n比如这里的ControlNotification有以下值\npub enum ControlNotification { RESIZED = 40i32, MOUSE_ENTER = 41i32, MOUSE_EXIT = 42i32, MOUSE_ENTER_SELF = 60i32, MOUSE_EXIT_SELF = 61i32, FOCUS_ENTER = 43i32, FOCUS_EXIT = 44i32, THEME_CHANGED = 45i32, SCROLL_BEGIN = 47i32, SCROLL_END = 48i32, LAYOUT_DIRECTION_CHANGED = 49i32, TRANSFORM_CHANGED = 2000i32, LOCAL_TRANSFORM_CHANGED = 35i32, DRAW = 30i32, VISIBILITY_CHANGED = 31i32, ENTER_CANVAS = 32i32, EXIT_CANVAS = 33i32, WORLD_2D_CHANGED = 36i32, ENTER_TREE = 10i32, EXIT_TREE = 11i32, MOVED_IN_PARENT = 12i32, READY = 13i32, PAUSED = 14i32, UNPAUSED = 15i32, PHYSICS_PROCESS = 16i32, PROCESS = 17i32, PARENTED = 18i32, UNPARENTED = 19i32, SCENE_INSTANTIATED = 20i32, DRAG_BEGIN = 21i32, DRAG_END = 22i32, PATH_RENAMED = 23i32, CHILD_ORDER_CHANGED = 24i32, INTERNAL_PROCESS = 25i32, INTERNAL_PHYSICS_PROCESS = 26i32, POST_ENTER_TREE = 27i32, DISABLED = 28i32, ENABLED = 29i32, RESET_PHYSICS_INTERPOLATION = 2001i32, EDITOR_PRE_SAVE = 9001i32, EDITOR_POST_SAVE = 9002i32, WM_MOUSE_ENTER = 1002i32, WM_MOUSE_EXIT = 1003i32, WM_WINDOW_FOCUS_IN = 1004i32, WM_WINDOW_FOCUS_OUT = 1005i32, WM_CLOSE_REQUEST = 1006i32, WM_GO_BACK_REQUEST = 1007i32, WM_SIZE_CHANGED = 1008i32, WM_DPI_CHANGE = 1009i32, VP_MOUSE_ENTER = 1010i32, VP_MOUSE_EXIT = 1011i32, WM_POSITION_CHANGED = 1012i32, OS_MEMORY_WARNING = 2009i32, TRANSLATION_CHANGED = 2010i32, WM_ABOUT = 2011i32, CRASH = 2012i32, OS_IME_UPDATE = 2013i32, APPLICATION_RESUMED = 2014i32, APPLICATION_PAUSED = 2015i32, APPLICATION_FOCUS_IN = 2016i32, APPLICATION_FOCUS_OUT = 2017i32, TEXT_SERVER_CHANGED = 2018i32, ACCESSIBILITY_UPDATE = 3000i32, ACCESSIBILITY_INVALIDATE = 3001i32, POSTINITIALIZE = 0i32, PREDELETE = 1i32, EXTENSION_RELOADED = 2i32, Unknown(i32), } 看着很多值，但里面其实有很多熟悉的名字，比如READY、PROCESS、ENTER_TREE这样的值\n实际上，在on_notification里面就靠这样识别是哪个事件的回调\n所以对于之前的示例，这样写就可以\n#[derive(GodotClass)] #[class(init, base=Node)] struct TestNode{} #[godot_api] impl INode for TestNode{ fn on_notification(\u0026amp;mut self, what: NodeNotification){ match what { NodeNotification::READY =\u0026gt; { godot_print!(\u0026#34;TestNode Ready\u0026#34;); } _ =\u0026gt; {} } } } 这样实现后再附加原来的脚本，脚本的_ready()就不会覆盖节点自己的READY逻辑了\n当然还有一个问题，运行后会发现先输出Script Ready然后输出TestNode Ready\n这是因为notification毕竟是回调，触发时间在每个事件的最后\n在这里就是脚本的_ready完成后才执行节点的NodeNotification::READY\n所以对于节点的初始化，应该用ENTER_TREE完成，毕竟到了ready的周期，节点应该已经初始化完毕\n番外：这对脚本实现的扩展有什么启示 如果说因为各种原因，扩展不打算通过GDExtension机制实现，这个知识有什么启示\n那还是有的，notification机制在C#/GDScript中也是可以实现的\n这样实现之后，对于继承类的脚本也不需要显式调用基类方法\n这里用GDscript简单实现一下\n首先创建addon，添加类型加载\n# res://addons/test_plugin/test_plugin.gd @tool extends EditorPlugin func _enable_plugin() -\u0026gt; void: # Add autoloads here. var script = load(\u0026#34;res://addons/test_plugin/test_node.gd\u0026#34;); add_custom_type(\u0026#34;TestNode\u0026#34;, \u0026#34;Node\u0026#34;, script, Texture2D.new()); pass func _disable_plugin() -\u0026gt; void: # Remove autoloads here. remove_custom_type(\u0026#34;TestNode\u0026#34;); pass func _enter_tree() -\u0026gt; void: # Initialization of the plugin goes here. pass func _exit_tree() -\u0026gt; void: # Clean-up of the plugin goes here. pass 创建新的类型脚本\n# res://addons/test_plugin/test_node.gd extend Node func _notification(what: int) -\u0026gt; void: if what == NOTIFICATION_READY: print(\u0026#34;TestNode Ready\u0026#34;); 然后新建场景，添加自定义的TestNode，然后扩展脚本\n# res://extend_test.gd extends \u0026#34;res://addons/test_plugin/test_node.gd\u0026#34; func _ready() -\u0026gt; void: print(\u0026#34;Script Ready\u0026#34;); 运行发现，也能达到之前的效果，先输出Script Ready然后输出TestNode Ready\n脚本实现的扩展的功能性问题也就剩类型没法通过ClassDB判断了\n不过之前也提到有其他方法判断，可以解决\n题外话，这样看的话C#在Godot真就算三等公民了\n虽然官方确实在努力支持C#，但论支持程度真的不太行，任重而道远\n目前按Godot社区的情况看，GDExtension是大伙都喜欢的方案，基本什么平台都可以导出\n而C#总是有这样或那样的限制，做GDExtension又不方便\n不负责任的讲，现在Godot对语言的支持，第一等绝对是GDScript，即开即用，支持全部功能\n第二等是有良好社区支持GDExtension的语言，比如Rust或者C++，通过GDExtension也可以支持全部功能\n然后就是第三等，总是受限的语言，C#属于是确实容易碰壁，其他的语言可能就是不好做GDExtension\n不过社区也在努力，对于GDExtension，有Godot-Sharp推进，C#的支持之后有望做成一个拓展包的形式\n只能说慢慢来吧，我也在尝试推进Godot上C#的支持，毕竟C#也算是我的白月光语言了\n","date":"2026-08-04T12:57:45+08:00","permalink":"/post/articles/2026/%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E6%8E%A5%E8%BF%91%E5%8E%9F%E7%94%9F%E7%9A%84gdextension%E7%B1%BB%E5%9E%8B/","title":"如何实现接近原生的GDExtension类型"},{"content":"vscode越来越重了，每次启动vscode都会吃一大堆内存，都赶上专门的IDE了\n（Visual Studio都比vscode省内存）\n所以又来折腾编辑器了\nneovim挺好的，但相对来说配置太多太杂，不好迁移\n经常配置lua脚本然后祈祷可以生效\nHelix一开始的开发目标就是IDE，所以很多功能是内置的，配置简单很多\n这里主要使用scoop作为包管理器，其他平台应该相应的包也好找\n安装Helix sudo scoop install helix -g LSP配置 Helix内置LSP的支持，对不同的语言，安装需要的LSP即可\n可通过命令行hx --health检查不同语言的支持情况\n常用语言 C/CPP sudo scoop install clangd -g CSharp sudo scoop install omnisharp -g Python sudo scoop install ruff -g Rust sudo scoop install rustup -g sudo scoop install llvm -g rustup component add rust-analyzer 配置文件 TOML sudo scoop install taplo -g 配置Helix 注意Windows的配置文件位置%APPDATA%/helix/config.toml\n其他系统在~/.config/helix/config.toml\n# config.toml theme = \u0026#34;tokyonight_moon\u0026#34; [editor] shell = [\u0026#34;pwsh\u0026#34;, \u0026#34;-c\u0026#34;] cursor-shape.insert = \u0026#34;bar\u0026#34; 就这样就可以了，配置非常方便\n","date":"2026-07-05T17:50:51+08:00","permalink":"/post/categories/backup/%E9%85%8D%E7%BD%AEhelix/","title":"配置Helix"},{"content":"从上一篇介绍我怎么搭博客的文章到现在也有些时间了\n期间，虽然样式都没怎么变，但还是优化了许多配置体验\n主题更新 现在，博客使用的是v4版本的Stack主题\n具体优化了什么确实看不太出来，但在配置方面做了很多优化\n首先是可以通过hugo module配置\n这个功能除了要安装hugo-extend还需要安装go\n配置详见Stack|开始使用\n这样配置之后更新主题只需要通过命令就可以了\nhugo mod get -u github.com/CaiJimmy/hugo-theme-stack/v4 hugo mod tidy 主题的配置文件从yaml变成了toml，大部分的键是没变的\n虽然也可以使用yaml，但为保险起见，还是使用toml\n下面给出我的配置以供参考\ntitle = \u0026#34;MLAcookie\u0026#34; defaultContentLanguage = \u0026#34;zh\u0026#34; [[module.imports]] path = \u0026#34;github.com/CaiJimmy/hugo-theme-stack/v4\u0026#34; [Params] favicon = \u0026#34;favicon.webp\u0026#34; rssFullContent = true comments.enabled = false [Params.sidebar] compact = false emoji = \u0026#34;🤯\u0026#34; subtitle = \u0026#34;随便记点东西的记事本\u0026#34; avatar = \u0026#34;img/avatar.png\u0026#34; [Params.widgets] homepage = [ { type = \u0026#34;search\u0026#34; }, { type = \u0026#34;archives\u0026#34;, params = { limit = 5 } }, { type = \u0026#34;categories\u0026#34;, params = { limit = 10 } }, { type = \u0026#34;tag-cloud\u0026#34;, params = { limit = 10 } }, ] page = [{ type = \u0026#34;toc\u0026#34; }] [Params.article] headingAnchor = false math = true toc = true readingTime = false [Params.article.license] enabled = true default = \u0026#34;CC BY-NC-SA 4.0\u0026#34; [Params.footer] since = 2023 [Params.colorScheme] toggle = false default = \u0026#34;dark\u0026#34; [[menu.social]] identifier = \u0026#34;github\u0026#34; name = \u0026#34;MLAcookie@Github\u0026#34; url = \u0026#34;https://github.com/MLAcookie\u0026#34; params.icon = \u0026#34;brand-github\u0026#34; [[menu.social]] identifier = \u0026#34;bilibili\u0026#34; name = \u0026#34;MLA_cookie@Bilibili\u0026#34; url = \u0026#34;https://space.bilibili.com/13629146\u0026#34; params.icon = \u0026#34;brand-bilibili\u0026#34; 脚本优化 托LLM的福，优化了大部分部署的脚本\n首先是全局配置脚本config.ps1\n添加了Github Page自定义Url需要的CNAME值\n# 获取脚本文件夹路径 $global:ShellScriptDir = Get-Location # 获取主目录 Set-Location .. $global:SiteDir = Get-Location Set-Location $global:ShellScriptDir # 构建目录 $global:PublicDir = Join-Path -Path $global:SiteDir -ChildPath \u0026#34;public\u0026#34; # 内容目录 $global:ContentDir = Join-Path -Path $global:SiteDir -ChildPath \u0026#34;content\u0026#34; # 基础URL $global:BaseURL = \u0026#34;https://XXX\u0026#34; # 远程Git仓库链接 $global:RemoteGitURL = \u0026#34;https://github.com/XXX/XXX\u0026#34; # 目标分支 $global:TargetBranch = \u0026#34;XXX\u0026#34; # Github Page CNAME $global:CNAME = \u0026#34;XXX\u0026#34; 然后是新建文章NewPost.ps1\n现在会自动根据目录名，添加categories的值\nparam( [string] [ValidateNotNullOrEmpty()] $PostName, [string] $CategoryName ) function ValidatedName { param ( [string] [ValidateNotNullOrEmpty()] $Name ) $SpecialCharacters = \u0026#39; !\u0026#34;$%\u0026amp;\u0026#39;\u0026#39;()*,-/:;\u0026lt;=\u0026gt;?@[\\]^_`{|}~\u0026#39;.ToCharArray() $Name = $Name.Replace(\u0026#34;`#\u0026#34;, \u0026#34;Sharp\u0026#34;) # 先每个替换成- foreach ($Character in $SpecialCharacters) { $Name = $Name.Replace($Character, \u0026#34;-\u0026#34;) } # 返回时使用正则表达式匹配连续的-，并替换为单个- return $Name -Replace \u0026#39;([-])+\u0026#39;, \u0026#39;-\u0026#39; } # 导入配置变量 . .\\configs.ps1 # 保存当前的工作路径 $localDir = Get-Location # 切换工作路径 Set-Location $global:SiteDir # 合法化路径名，不然图片会出问题 $validPath = ValidatedName -Name $PostName # 普通文章创建 if ($CategoryName -eq \u0026#34;\u0026#34;) { # 新建Hugo文章 $newPostPath = Join-Path -Path \u0026#34;content\\post\\articles\u0026#34; -ChildPath (Get-Date -Format \u0026#34;yyyy\u0026#34;) $newPostPath = Join-Path -Path (Join-Path -Path $newPostPath -ChildPath $validPath) -ChildPath \u0026#34;index.md\u0026#34; hugo new --kind NormalArticle $newPostPath } # 需要分类的文章 else { # 新建Hugo文章 $categoryPath = Join-Path -Path \u0026#34;content\\post\\categories\u0026#34; -ChildPath $CategoryName $newPostPath = Join-Path -Path (Join-Path -Path $categoryPath -ChildPath $validPath) -ChildPath \u0026#34;index.md\u0026#34; hugo new --kind CategorizedArticle $newPostPath $fullPath = Join-Path -Path $global:SiteDir -ChildPath $newPostPath (Get-Content -Path $fullPath -Raw) -replace \u0026#39;categories: \u0026#34;\u0026#34;\u0026#39;, \u0026#34;categories: \u0026#34;\u0026#34;$CategoryName\u0026#34;\u0026#34;\u0026#34; | Set-Content -Path $fullPath -NoNewline } # 返回之前的工作路径 Set-Location $localDir 最后是部署脚本Deploy.ps1\n添加了自动复制WebMaster的认证文件，自动添加CNAME标识文件\nparam( [switch] $Force ) function Invoke-GitInit { param ( [switch] $Force ) Push-Location $global:PublicDir git init git remote add origin $global:RemoteGitURL git branch -u $global:TargetBranch Invoke-GitPush -Force:$Force Pop-Location } function Invoke-GitPush { param( [switch]$Force ) Push-Location $global:PublicDir git pull git add . git commit -m (\u0026#34;Deploy: \u0026#34; + (Get-Date -Format \u0026#34;yyMMdd-HH:mm\u0026#34;)) if ($Force) { git push origin HEAD:$global:TargetBranch --force } else { git push origin HEAD:$global:TargetBranch } Pop-Location } function Export-WebInfo { Push-Location $global:PublicDir if ($global:CNAME -and -not (Test-Path \u0026#34;CNAME\u0026#34;)) { Set-Content -Path \u0026#34;CNAME\u0026#34; -Value $global:CNAME } $authFile = Join-Path -Path $global:ShellScriptDir -ChildPath \u0026#34;BingSiteAuth.xml\u0026#34; if (Test-Path $authFile) { Copy-Item -Path $authFile -Destination \u0026#34;BingSiteAuth.xml\u0026#34; } Pop-Location } # 导入配置变量 . .\\configs.ps1 # 保存当前的工作路径 $localDir = Get-Location # 切换工作路径 Set-Location $global:SiteDir # 清除Hugo构建缓存 if ($Force) { # 递归删除所有文件 Remove-Item -Path (Join-Path -Path $global:PublicDir -ChildPath \u0026#34;*\u0026#34;) -Recurse } # 构建网页 hugo --gc --minify --baseURL $global:BaseURL Export-WebInfo # 准备推送 # 没有初始化git的化就先初始化并强制推送 if (-not (Test-Path (Join-Path -Path $global:PublicDir -ChildPath \u0026#34;.git\u0026#34;))) { Invoke-GitInit -Force } else { Invoke-GitPush -Force:$Force } # 返回之前的工作路径 Set-Location $localDir ","date":"2026-07-04T00:40:36+08:00","permalink":"/post/categories/blog/26%E5%B9%B4%E5%8D%9A%E5%AE%A2%E7%8A%B6%E5%86%B5%E6%9B%B4%E6%96%B0/","title":"26年博客状况更新"},{"content":"源自世嘉MD的技术文档，YM2612部分\n概述 YM2612是一个使用FM合成技术的声音合成芯片\n具有下列能力\n6个FM通道 1个8-bit通道（可替换其中的一个FM通道） 立体声输出 1个低频振荡器（LFO），用于失真调频声音 2个计时器，可在软件中使用 标准的FM通道各有一个整体频率，以及如何将该频率转换为复杂最终波形（即音色）的数据\n这种转换过程使用四个专用的通道组件，称为“算子”\n每个算子都拥有一个频率（整体频率的变体）、一个包络，以及利用其频率和包络来调制输入信号的能力\n算子频率是整体频率的整数倍偏移量\n共有两组FM通道，每组三个，分别命名为通道1-3和通道4-6\n通道3和通道6，能够为每个运算器使用完全独立的频率，而不是整数倍的偏移量\n这对于打击乐器效果很好，打击乐器的泛音通常位于基频的1.4倍或1.7倍等奇数倍处\n8-bit音频通道可替换FM的通道6，这意味着开启DAC（数模转换器）会关闭FM通道6\n这意味着，除非软件经过非常精巧的构造，否则不可能在DAC工作的同时使用任何FM通道\n大量游戏实际上通过软件构造做到了这一点\nLFO，即低频振荡器，允许对FM声音进行幅度和/或频率的失真处理\n每个通道可以选择其受LFO影响的程度（如果有的话）\n关于算子 每个通道都分配有4个专属的算子，其属性如下：\n每个算子都有输入、用于调制输入的频率和包络，以及输出。 算子分为两种类型：调制器与载波器 调制器，算子产生的波形用来调制接下来的波形 载波器，算子产生的波形会输出为音频 算子可反馈到自身，从而产生更复杂的波形。 这些算子可按8种不同配置排列，称为“算法”。 算法编号 推荐用途 0 失真电吉他、高音鼓切片、贝斯 1 竖琴、可编程声音（PSG） 2 贝斯、电吉他、铜管乐、钢琴、木管乐 3 弦乐、民谣吉他、编钟 4 笛子、铃铛、和声、底鼓、小军鼓、咚咚鼓 5 铜管乐、风琴 6 木琴、咚咚鼓、风琴、颤音琴、底鼓、小军鼓 7 管风琴 包络规范 当琴键被按下时，声音开始发出，这个过程称为“键开”（key on）\n声音具有一个起音阶段、一次较强的初始衰减，随后是一次较慢的二次衰减\n声音会持续进行这种二次衰减，直到琴键被释放，这个过程称为“键关”（key off）\n此后，声音开始快速最终衰减，例如钢琴键释放后制音器落在琴弦上时的情况\n上述的幅值和角度通过若干参数表示\n参数名 描述 TL 总电平（Total Level），即波形的最高幅值 AR 起音速率（Attack Rate），即初始幅值上升的角度 D1R 初始幅值衰减的角度 T1L 较慢幅值衰减开始时的幅值 D2R 二次幅值衰减的角度 RR “键关”之后的最终幅值衰减角度 还有额外的参数\n参数名 描述 RS 速率缩放（Rate Scaling），即包络随频率升高而变短的程度 AM 幅值调制（Amplitude Modulation），指定该运算器是否允许被LFO调制 SSG-EG 包络发生器，是一种特殊的低频信号发生器，可用于调制音色通道的输出 ","date":"2026-07-03T17:15:04+08:00","permalink":"/post/articles/2026/ym2612%E7%9A%84%E6%8A%80%E6%9C%AF%E7%BB%86%E8%8A%82/","title":"YM2612的技术细节"},{"content":"在AI横行的当下，作为搞程序的那一定是要与时俱进的\n我主要使用Opencode作为我的AI Agent实现，这里记录自己的配置\n安装Bun Bun是一个高性能的js runtime，配置简单好用\n这里通过scoop安装\nsudo scoop install bun -g 安装Opencode bun install -g opencode-ai ","date":"2026-06-22T21:01:36+08:00","permalink":"/post/categories/backup/%E9%85%8D%E7%BD%AEopencode/","title":"配置Opencode"},{"content":"WSL可以非常轻松的创建需要的Linux开发环境\nBut I use Arch by the way\n在WSL中安装Arch也是非常简单的，但要正常用，还需要一些配置\n为什么选择Arch\n因为Arch足够轻量，比起别的发行版，本身占用不大\n并且有AUR这样丰富的软件源，不用在安装软件前先配置一遍软件源\n首先是安装Arch\nwsl --install archlinux --location [希望安装的位置] WSL默认安装是在C盘的，但很多时候C盘的空间比较宝贵\n这样安装可以让Arch的虚拟磁盘文件放在希望的位置\n安装后，我们发现进去就是root，这对一些操作是不利的\n权限过高，容易误操作把系统干烂\n而且有些工具是不允许在root环境下运行的，比如makepkg\n所以第一步是先创建一个自己用的用户\nuseradd -m -G wheel [UserName] passwd [UserName] # 更新软件库 pacman -Syyu pacman -S vi sudo visudo # 删除井号 %wheel ALL=(ALL) ALL 这里的wheel用户组是Arch的默认用户组之一\n这个用户组有权使用诸如pacman等管理工具\n然后配置WSL启动行为，让新创建的用户作为默认登入用户\nvi /etc/wsl.conf # 添加下列内容 [user] default=[UserName] 这里再配置下AUR，这里选择yay作为管理器\npacman -S base-devel git git clone https://aur.archlinux.org/yay.git --depth=1 cd yay makepkg -si vi /etc/pacman.conf # 删除井号 color 现在，WSL的Arch就配置完成了\n","date":"2026-04-10T16:26:57+08:00","permalink":"/post/articles/2026/%E5%9C%A8wsl%E4%B8%AD%E9%85%8D%E7%BD%AEarch/","title":"在WSL中配置Arch"},{"content":"由于国内特殊的网络环境，一些国外的开发组件访问有障碍\n如果使用代理地址的方式，不同工具的proxy设置需要特殊设置\n这里收集我遇到的proxy设置项\nShell 通过使用环境变量的方式实现\n大部分软件可以通过这个环境变量设置代理\nexport http_proxy=\u0026#34;http://127.0.0.1:1000\u0026#34; export https_proxy=\u0026#34;http://127.0.0.1:1000\u0026#34; 常见工具 Git git config set --global http.proxy http://127.0.0.1:1000 git config set --global https.proxy http://127.0.0.1:1000 Curl 兼容Shell的Proxy设置\nwget 兼容Shell的Proxy设置\nDocker nano ~/.docker/config.json { \u0026#34;proxies\u0026#34;: { \u0026#34;default\u0026#34;: { \u0026#34;httpProxy\u0026#34;: \u0026#34;http://127.0.0.1:1000\u0026#34;, \u0026#34;httpsProxy\u0026#34;: \u0026#34;http://127.0.0.1:1000\u0026#34;, } } } JavaScript Bun 兼容Shell的Proxy设置\nNpm npm config set proxy http://127.0.0.1:1000 npm config set https-proxy http://127.0.0.1:1000 ","date":"2026-03-23T19:49:24+08:00","permalink":"/post/categories/backup/%E5%90%84%E7%A7%8D%E5%B7%A5%E5%85%B7proxy%E9%85%8D%E7%BD%AE/","title":"各种工具Proxy配置"},{"content":"收集一些使用wsl中遇到的小问题\n当然，其中也不只是wsl的问题，也有一些linux中常见问题\n使用systemd初始化系统 在wsl中，默认使用init初始化系统而不是systemd\n要使用systemd就需要写配置文件启用功能\n首先需确保使用的是wsl2版本\nwsl -l -v # 如果不是升级一下 wsl --set-version \u0026lt;发行版名称\u0026gt; 2 然后编辑配置文件\nsudo nano /etc/wsl.conf # 添加以下内容 [boot] systemd=true 使用systemd会让系统行为更接近常用发行版的行为\n大部分的wsl发行版应该都启用了systemd\nRider连接WSL中的Docker 不配置可能会在Rider连接WSL中的Docker时报Permission Denied\n因为Rider没有足够的权限去访问Docker守护进程的socket文件（/var/run/docker.sock）\n这个文件需要root用户或docker用户组的权限\n# 将当前用户加入 docker 组 sudo usermod -aG docker $USER # 验证用户是否已在组中 groups $USER 然后重启下WSL即可\n","date":"2026-03-23T17:16:07+08:00","permalink":"/post/articles/2026/%E4%BD%BF%E7%94%A8wsl%E7%9A%84%E4%B8%80%E4%BA%9B%E5%B0%8F%E9%97%AE%E9%A2%98/","title":"使用wsl的一些小问题"},{"content":"我们服务器搬机房，原来的虚拟机需要重建\n所有的镜像在LVM-Thin逻辑卷中，理论上是可以直接挂载的\n但Linux中的/dev下面没有发现对应的镜像\n排查了一圈后发现是卷未激活\n# 查看哪些卷未激活 lvscan # 激活所有卷 vgchange -ay # 为虚拟机配置硬盘 qm set [虚拟机ID] --scsi0 [镜像读写位置] 有些情况下，宿主机重启后没有激活这些逻辑卷\n这时候可以考虑通过udev规则实现\ncd /etc/udev/rules.d # 创建新规则 nano 99-lvm-persistent.rules # 规则内容如下 ACTION==\u0026#34;add\u0026#34;, ENV{ID_FS_TYPE}==\u0026#34;LVM2_member\u0026#34;, RUN+=\u0026#34;/sbin/vgchange -ay\u0026#34; # 重新加载udev规则 udevadm control --reload-rules udevadm trigger ","date":"2026-01-07T21:41:21+08:00","permalink":"/post/articles/2026/pve%E4%B8%AD%E6%8C%82%E8%BD%BDlvm-thin%E4%B8%AD%E7%9A%84%E9%95%9C%E5%83%8F/","title":"PVE中挂载LVM-Thin中的镜像"},{"content":"Godot的GDExtension通过运行时动态加载编译完成的Dll文件\n实现对编辑器功能的扩展，并且无需重新编译引擎源代码\n虽然Godot官方确实有这样的教程，但在我实际按照教程操作时，发现很多已过时的内容\n本文章采用的版本是4.5\n从导出符号文件里面的注释，GDExtension的API应该是时常修改的，所以与教程有差别也正常\n并且Godot官方的教程存在部分未翻译的情况，所以觉得还是要写点东西记一下\n本文章不大会对函数的具体功能做介绍，只是让GDExtension可以跑起来\n代码仓库\n使用C开发GDExtension \u0026lt;0x00\u0026gt; 从Godot中导出GDExtension符号 首先要获取GDExtension需要的符号文件\n这一步需要确保你的Godot执行文件可以在命令行中找到\n（这里推荐通过scoop安装Godot，会自动配置这一块的）\n命令行中执行下列语句\ngodot --dump-gdextension-interface godot --dump-extension-api 这样会得到两个文件，分别是gdextension_interface.h与extension_api.json\ngdextension_interface.h包括了编译时需要的符号信息，里面都是C风格的定义\nextension_api.json包含了Godot类型的信息，包括类型大小等，编译时不需要，只是供代码编写时使用\n\u0026lt;0x01\u0026gt; 配置目录结构与构建系统 我们主要分两个文件夹来组织文件\ngdextension_c_example\r├── demo # 编译输出文件夹\r└── src # 源代码文件夹 需要把gdextension_interface.h放到src文件夹中\n官方的教程使用Scons作为构建系统，但这个我不熟，所以我换成了我更熟悉的Xmake\n因为最后只要能输出相应的动态链接库即可，所以其实构建系统是可以挑自己喜欢的\n这里使用到的Xmake.lua如下\nadd_rules(\u0026#34;mode.debug\u0026#34;, \u0026#34;mode.release\u0026#34;) target(\u0026#34;ExtensionTest\u0026#34;) set_kind(\u0026#34;shared\u0026#34;) set_toolchains(\u0026#34;clang\u0026#34;) set_targetdir(\u0026#34;demo/bin\u0026#34;) add_files(\u0026#34;src/*.c\u0026#34;) 这里还指定了使用clang工具链，只是因为我比较喜欢clang，用其他的也问题不大\n\u0026lt;0x02\u0026gt; 编写代码 对于GDExtension，Godot通过一个函数入口来初始化调用\n函数入口的名字无所谓，只要可以和之后的配置文件对上即可\n这里假设入口函数名为gdexample_library_init，入口函数签名是这样的\nGDExtensionBool gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) // GDExtensionBool: uint_8 // GDExtensionInterfaceGetProcAddress: void(*(*)(const char*))() // GDExtensionClassLibraryPtr: void* // GDExtensionInitialization: (struct) 某种程度上，只写这样一个函数就可以\n但这样会导致代码很难维护，所以需要将一些东西分离出来，方便开发\n扩展配置文件 首先先在demo文件夹下面创建插件的配置文件gdexample.gdextension\n# ./demo/gdexample.gdextension\r[configuration]\rentry_symbol = \u0026#34;gdexample_library_init\u0026#34;\rcompatibility_minimum = \u0026#34;4.2\u0026#34;\r[libraries]\rmacos.debug = \u0026#34;res://addon/bin/ExtensionTest.dylib\u0026#34;\rlinux.debug = \u0026#34;res://addon/bin/ExtensionTest.so\u0026#34;\rwindows.debug = \u0026#34;res://addon/bin/ExtensionTest.dll\u0026#34; entry_symbol表示函数入口符号，能对应上即可\n下面的[libraries]指示Godot该在哪里找到动态链接库\n扩展的其他符号定义 gdextension_interface.h只导出了必须的GDExtensionAPI\n而Godot的很多其他部分如StringName的符号定义并未给出\n所以需要有一个定义其他符号的文件\n创建./src/defs.h文件，内容如下\n#pragma once #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stddef.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #if !defined(GDE_EXPORT) #if defined(_WIN32) #define GDE_EXPORT __declspec(dllexport) #elif defined(__GNUC__) #define GDE_EXPORT __attribute__((visibility(\u0026#34;default\u0026#34;))) #else #define GDE_EXPORT #endif #ifdef BUILD_32 #define STRING_NAME_SIZE 4 #else #define STRING_NAME_SIZE 8 #endif typedef struct { uint8_t data[STRING_NAME_SIZE]; } StringName; #endif // ! GDE_EXPORT 这里主要定义了两个东西，一个是GDE_Export宏，另一个是StringName的符号定义\nGDE_Export宏需要添加在需要导出到动态链接库的函数声明中\nStringName的类型大小信息可以在extension_api.json中寻找\nAPI绑定 我们可以通过函数入口传入的p_get_proc_address获取需要的函数\n为了方便调用与减少代码错误，这里为需要的用到的函数创建符号\n// ./src/api.h #pragma once #include \u0026#34;gdextension_interface.h\u0026#34; #include \u0026#34;defs.h\u0026#34; extern GDExtensionClassLibraryPtr class_library; extern struct Constructors { GDExtensionInterfaceStringNameNewWithLatin1Chars string_name_new_with_latin1_chars; } constructors; extern struct Destructors { GDExtensionPtrDestructor string_name_destructor; } destructors; extern struct API { GDExtensionInterfaceClassdbRegisterExtensionClass2 classdb_register_extension_class2; GDExtensionInterfaceClassdbConstructObject classdb_construct_object; GDExtensionInterfaceObjectSetInstance object_set_instance; GDExtensionInterfaceObjectSetInstanceBinding object_set_instance_binding; GDExtensionInterfaceMemAlloc mem_alloc; GDExtensionInterfaceMemFree mem_free; } api; void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address); // ./src/api.c #include \u0026#34;api.h\u0026#34; struct Constructors constructors; struct Destructors destructors; struct API api; GDExtensionClassLibraryPtr class_library = NULL; void load_api(GDExtensionInterfaceGetProcAddress p_get_proc_address) { // Get helper functions first. GDExtensionInterfaceVariantGetPtrDestructor variant_get_ptr_destructor = (GDExtensionInterfaceVariantGetPtrDestructor)p_get_proc_address(\u0026#34;variant_get_ptr_destructor\u0026#34;); // API. api.classdb_register_extension_class2 = p_get_proc_address(\u0026#34;classdb_register_extension_class2\u0026#34;); // Constructors. constructors.string_name_new_with_latin1_chars = (GDExtensionInterfaceStringNameNewWithLatin1Chars)p_get_proc_address(\u0026#34;string_name_new_with_latin1_chars\u0026#34;); // Destructors. destructors.string_name_destructor = variant_get_ptr_destructor(GDEXTENSION_VARIANT_TYPE_STRING_NAME); api.classdb_register_extension_class2 = p_get_proc_address(\u0026#34;classdb_register_extension_class2\u0026#34;); api.classdb_construct_object = (GDExtensionInterfaceClassdbConstructObject)p_get_proc_address(\u0026#34;classdb_construct_object\u0026#34;); api.object_set_instance = p_get_proc_address(\u0026#34;object_set_instance\u0026#34;); api.object_set_instance_binding = p_get_proc_address(\u0026#34;object_set_instance_binding\u0026#34;); api.mem_alloc = (GDExtensionInterfaceMemAlloc)p_get_proc_address(\u0026#34;mem_alloc\u0026#34;); api.mem_free = (GDExtensionInterfaceMemFree)p_get_proc_address(\u0026#34;mem_free\u0026#34;); } 这样通过全局字段绑定了需要使用的api，方便调用\n自定义节点类型 现在可以开始写自定义节点的实现了\n// ./src/gdextension.h #pragma once #include \u0026#34;gdextension_interface.h\u0026#34; #include \u0026#34;defs.h\u0026#34; // Struct to hold the node data. typedef struct { GDExtensionObjectPtr object; // Stores the underlying Godot object. } CookieGDExample; // Constructor for the node. void gdexample_class_constructor(CookieGDExample *self); // Destructor for the node. void gdexample_class_destructor(CookieGDExample *self); // Bindings. void gdexample_class_bind_methods(); GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata); void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance); // ./src/gdextension.c #include \u0026#34;gdexample.h\u0026#34; #include \u0026#34;api.h\u0026#34; void gdexample_class_constructor(CookieGDExample *self) { } void gdexample_class_destructor(CookieGDExample *self) { } void gdexample_class_bind_methods() { } const GDExtensionInstanceBindingCallbacks gdexample_class_binding_callbacks = { .create_callback = NULL, .free_callback = NULL, .reference_callback = NULL, }; GDExtensionObjectPtr gdexample_class_create_instance(void *p_class_userdata) { // 先创建Sprite2D节点 // 创建临时的StringName结构 StringName class_name; // 给创建的StringName结构赋值\u0026#34;Sprite2D\u0026#34; constructors.string_name_new_with_latin1_chars(\u0026amp;class_name, \u0026#34;Sprite2D\u0026#34;, false); // 通过ClassDB创建Sprite2D节点 GDExtensionObjectPtr object = api.classdb_construct_object(\u0026amp;class_name); // 销毁临时的StringName数据 destructors.string_name_destructor(\u0026amp;class_name); // 创建扩展类型节点 // 给扩展类型节点分配内存 CookieGDExample *self = (CookieGDExample *)api.mem_alloc(sizeof(CookieGDExample)); // 调用扩展类型节点构造函数，这里演示为空 gdexample_class_constructor(self); // 给扩展类型赋值 self-\u0026gt;object = object; // 配置扩展类型节点 // 给临时StringName结构赋值\u0026#34;GDExample\u0026#34; constructors.string_name_new_with_latin1_chars(\u0026amp;class_name, \u0026#34;GDExample\u0026#34;, false); // 设置创建的object api.object_set_instance(object, \u0026amp;class_name, self); // 配置object绑定的脚本之类的，跟这里填NULL即可 api.object_set_instance_binding(object, class_library, self, \u0026amp;gdexample_class_binding_callbacks); // 销毁临时的StringName数据 destructors.string_name_destructor(\u0026amp;class_name); return object; } void gdexample_class_free_instance(void *p_class_userdata, GDExtensionClassInstancePtr p_instance) { if (p_instance == NULL) { return; } CookieGDExample *self = (CookieGDExample *)p_instance; gdexample_class_destructor(self); api.mem_free(self); } 初始化模块 有上面的工作后，就可以开始初始化模块了\n// ./src/init.h #pragma once #include \u0026#34;defs.h\u0026#34; #include \u0026#34;gdextension_interface.h\u0026#34; void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level); void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level); GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization); // ./src/init.c #include \u0026#34;init.h\u0026#34; #include \u0026#34;api.h\u0026#34; #include \u0026#34;gdexample.h\u0026#34; void initialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level) { if (p_level != GDEXTENSION_INITIALIZATION_SCENE) { return; } StringName class_name; constructors.string_name_new_with_latin1_chars(\u0026amp;class_name, \u0026#34;GDExample\u0026#34;, false); StringName parent_class_name; constructors.string_name_new_with_latin1_chars(\u0026amp;parent_class_name, \u0026#34;Sprite2D\u0026#34;, false); // 创建类型信息 GDExtensionClassCreationInfo2 class_info = { .is_virtual = false, .is_abstract = false, .is_exposed = true, .set_func = NULL, .get_func = NULL, .get_property_list_func = NULL, .free_property_list_func = NULL, .property_can_revert_func = NULL, .property_get_revert_func = NULL, .validate_property_func = NULL, .notification_func = NULL, .to_string_func = NULL, .reference_func = NULL, .unreference_func = NULL, .create_instance_func = gdexample_class_create_instance, .free_instance_func = gdexample_class_free_instance, .recreate_instance_func = NULL, .get_virtual_func = NULL, .get_virtual_call_data_func = NULL, .call_virtual_with_data_func = NULL, .get_rid_func = NULL, .class_userdata = NULL, }; // 把类型信息注册到ClassDB中 api.classdb_register_extension_class2(class_library, \u0026amp;class_name, \u0026amp;parent_class_name, \u0026amp;class_info); // Bind methods. gdexample_class_bind_methods(); // 销毁两个临时的StringName数据 destructors.string_name_destructor(\u0026amp;class_name); destructors.string_name_destructor(\u0026amp;parent_class_name); } void deinitialize_gdexample_module(void *p_userdata, GDExtensionInitializationLevel p_level) { } GDExtensionBool GDE_EXPORT gdexample_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization) { class_library = p_library; load_api(p_get_proc_address); r_initialization-\u0026gt;initialize = initialize_gdexample_module; r_initialization-\u0026gt;deinitialize = deinitialize_gdexample_module; r_initialization-\u0026gt;userdata = NULL; r_initialization-\u0026gt;minimum_initialization_level = GDEXTENSION_INITIALIZATION_SCENE; return true; } （GDExtensionClassCreationInfo2算是废弃了，4.5应该用GDExtensionClassCreationInfo5）\n（不过如果考虑给旧版本开发GDExtension，那应该使用旧版本的函数）\n\u0026lt;0x03\u0026gt; 在Godot中使用 只需要创建新项目，然后将demo文件夹直接复制到项目文件中\n需要注意gdexample.gdextension内的路径\n不同于通过脚本实现的插件需要启用\nGDExtension实现的插件在Godot读取到*.gdextension后，会自动开始加载需要的动态链接文件\n当然，如果在初始化模块时遇到错误，会导致整个Godot直接崩溃\n加载后，就可以在添加新节点中看到扩展定义的节点\n使用C#开发GDExtension 上面提到Godot通过运行时加载动态链接库实现对引擎功能的扩充\n而理论上，加载动态链接库实际上只要调用的符号对得上就可以，这是与语言无关的\n也就是说，只要语言可以导出C符号的动态链接库，就可以写GDExtension\n而C#恰好就可以通过AOT导出C符号的动态链接库，这就很有意思了\n\u0026lt;0x00\u0026gt; 使用C#开发的前言 需要注意的是，在我研究到一半的时候，发现有项目在做这个事情了(godot-dotnet)\n当然，这个项目还不像Rust或者Go的Binding那么完善稳定，不推荐在生产环境中使用\n所以，这篇文章更多的是举一个例子，展示如何用C#编写AOT的代码\n代码仓库\n需要注意的是，编写AOT代码不仅需要.netSDK，还需要额外的工具来完成符号的导出\n比如说Windows平台上，就需要安装Visual Studio的C++工作负载\n主要是需要MSVC生成工具，其他平台可以参考微软的这篇教程\n\u0026lt;0x01\u0026gt; 一些概念 unsafe代码块 在本项目中，我们需要用到一些unsafe的代码\n在unsafe上下文中，我们可以通过指针直接操作内存\n代价就是这样没有运行时的内存安全保证\n一旦操作不当就可能会引起程序崩溃\n不过对于一些本机代码的交互，还是需要用unsafe的\n函数指针 在GDExtension中，有非常多的操作是通过函数指针传递的\n对于C中如下的函数指针\ntypedef void (*function_a)(int num); typedef int (*function_b)(const char* str); C#中需要如下方式声明\nusing FunctionAPtr = delegate* unmanaged[Cdecl]\u0026lt;int, void\u0026gt;; // C的char类型应该对应C#的byte，C#的char类型为2字节，表示UTF16编码 using FunctionBPtr = delegate* unmanaged[Cdecl]\u0026lt;byte*, int\u0026gt;; 有一说一，写法上确实挺抽象的\n这里使用的是using声明，作用域仅限当前文件内\n如果需要确保类型，就需要单独声明一个struct，比如这样\npublic unsafe struct FunctionAPtr { private FunctionAPtr(delegate* unmanaged[Cdecl]\u0026lt;int, void\u0026gt; ptr) { _ptr = ptr; } private delegate* unmanaged[Cdecl]\u0026lt;int, void\u0026gt; _ptr; public static implicit operator FunctionAPtr(delegate* unmanaged[Cdecl]\u0026lt;int, void\u0026gt; ptr) =\u0026gt; new (ptr); } 由于struct的内存分配特性，FunctionAPtr结构位置也即_ptr的位置\n这就相当于给指针分配了类型\n需要注意的是，在C#中指定方法需要在非托管环境中调用，需要用一个特性标记\n// 标记仅允许非托管环境调用，调用约定为Cdecl [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] public static void FunctionA(int num) { } delegate* unmanaged[Cdecl]\u0026lt;int, void\u0026gt; ptr = \u0026amp;FunctionA; fixed代码块 C#中有一个挺少见的关键字fixed\n这个是用来指定该代码区域中指定托管对象的内存位置不动\n为什么需要这样代码块呢，因为托管对象在托管堆的内存位置是有可能改变的\n由于垃圾回收机制的存在，不需要的对象会被清理\n而为了避免内存空间碎片化，垃圾回收器会对剩余的对象重新排列\n通过fixed关键字，告诉垃圾回收器在这里暂时不要动指定对象的内存位置\n下面是一个示例\npublic class Test { public DoSomething() { // ... // 固定this表示的对象 fixed(void* pThis = \u0026amp;this) { Method(pThis); } // ... } } \u0026lt;0x02\u0026gt; 开始 创建一个项目，这里采用.net10，使用类库项目模板\n需要在.csproj文件中启用AOT与unsafe上下文\n\u0026lt;Project Sdk=\u0026#34;Microsoft.NET.Sdk\u0026#34;\u0026gt; \u0026lt;PropertyGroup\u0026gt; \u0026lt;TargetFramework\u0026gt;net10.0\u0026lt;/TargetFramework\u0026gt; \u0026lt;ImplicitUsings\u0026gt;enable\u0026lt;/ImplicitUsings\u0026gt; \u0026lt;Nullable\u0026gt;enable\u0026lt;/Nullable\u0026gt; \u0026lt;PublishAot\u0026gt;true\u0026lt;/PublishAot\u0026gt; \u0026lt;AllowUnsafeBlocks\u0026gt;true\u0026lt;/AllowUnsafeBlocks\u0026gt; \u0026lt;/PropertyGroup\u0026gt; \u0026lt;/Project\u0026gt; 整个入口 首先先复刻一个我们需要的函数入口\n// .ExtensionEntry.cs using System.Runtime.CompilerServices; using System.Runtime.InteropServices; public unsafe class ExtensionEntry { [UnmanagedCallersOnly(EntryPoint = \u0026#34;aot_init\u0026#34;)] public static bool Init( nint pGetProcAddress, nint pLibrary, nint rInitialization) { } } 其他先不加，尝试生成一下，通过dumpbin看看有没有这个入口\n有aot_init就说明已经把入口符号导出了\n分离代码 为了方便管理，这里将不同的代码放在不同的文件中\n./GdExtensionApi/GodotApi.cs负责保存需要的Api函数指针 ./GdExtensionApi/GodotEnum.cs定义需要的枚举类型 ./GdExtensionApi/GodotFunction.cs定义需要的函数指针类型封装 ./GdExtensionApi/GodotStruct.cs定义需要的结构 ./GdExtensionApi/GodotValue.cs定义需要的数值类型 \u0026lt;0x03\u0026gt; 实现 具体流程实际上还是与C的示例是一致的\n也是通过入口，获取所有需要的Api函数指针\n然后再进行一些操作\n由于C#没有这些符号定义，所以最花时间的就是定义C#中的符号\n自己定义的符号通过一些方式还可以融合C#的语法糖，也还行\nStringName定义 GDExtension中，获取StringName实际上获取的是StringName的指针\n其内容的生存期由Godot本身管理\n也就是说，在我们的代码中需要告诉Godot什么时候销毁这个东西\n所以会在C的代码中看到这样的代码\nStringName class_name; // 创建StringName，内容为\u0026#34;Sprite2D\u0026#34; constructors.string_name_new_with_latin1_chars(\u0026amp;class_name, \u0026#34;Sprite2D\u0026#34;, false); // 一些操作 // 指示销毁 destructors.string_name_destructor(\u0026amp;class_name); 这会导致局部暂时使用StringName会有些麻烦，需要记得销毁\nC#中有个using语法，表示在作用域内使用的变量，离开作用域后自动销毁\n这依赖于IDisposable接口的实现\n我们可以在自己定义的StringName中实现这个接口\npublic unsafe struct StringName : IDisposable { private StringName(string str) { From(str); } // 需要加载的函数指针 public static delegate* unmanaged[Cdecl]\u0026lt;StringName*, byte*, GdExtensionBool, void\u0026gt; ConstructPtr; public static delegate* unmanaged[Cdecl]\u0026lt;StringName*, void\u0026gt; DestructPtr; public static implicit operator StringName(string str) =\u0026gt; new(str); private void* _ptr = null; private void From(string str) { fixed (StringName* pThis = \u0026amp;this) { ConstructPtr(pThis, (byte*)Marshal.StringToHGlobalAnsi(str), false); } } public void Dispose() { fixed (StringName* pThis = \u0026amp;this) { DestructPtr(pThis); } } } 这样的实现，在代码中需要使用StringName时，就方便许多了\nusing StringName className = \u0026#34;Sprite2D\u0026#34;; 其他类型的封装 在本示例中，其他类型的封装大多是重复劳动\n主要使用的技巧是隐式类型转换，比如说这样\npublic unsafe struct GdExtensionObjectPtr { private GdExtensionObjectPtr(void* ptr) { _ptr = ptr; } private readonly void* _ptr; public static implicit operator GdExtensionObjectPtr(void* ptr) =\u0026gt; new(ptr); public static implicit operator void*(GdExtensionObjectPtr ptr) =\u0026gt; ptr._ptr; } 定义了void*与GdExtensionObjectPtr类型的互相隐式转换\n在需要的地方就可以非常方便得转换不同类型的指针封装\n具体的话可以看仓库代码\n枚举类型的传递 GDExtension中也存在一些枚举类型，这些类型在C#中怎么对应呢\n好消息是，在C与C#中，枚举本质上与int类似\n传递不同的枚举值相当于传递不同的int值\n也就是可以直接对应，只要C#中的定义与C中的定义一致即可\n具体详见仓库代码\n编写自定义节点 有了上面的定义基础，我们就可以用C#编写一个简单的节点了\n上次用C写的是继承Sprite2D的节点，这次写一个继承Control的节点\n// ./AotExample.cs using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using CSharpAotExtension.GodotExtensionApi; namespace CSharpAotExtension; public unsafe struct AotExample { private static readonly GdExtensionInstanceBindingCallbacks BindingCallbacks; private GdExtensionObjectPtr _objectPtr; [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] public static GdExtensionObjectPtr CreateInstance(void* pClassUserdata) { using StringName controlName = \u0026#34;Control\u0026#34;; using StringName exampleName = \u0026#34;AotExample\u0026#34;; var control = GdExtensionClassDb.ConstructObject(\u0026amp;controlName); var self = (AotExample*)GdExtensionCore.MemAlloc(sizeof(AotExample)); self-\u0026gt;_objectPtr = control; GdExtensionCore.ObjectSetInstance(control, \u0026amp;exampleName, self); fixed (GdExtensionInstanceBindingCallbacks* pCallbacks = \u0026amp;BindingCallbacks) { GdExtensionCore.ObjectSetInstanceBinding(control, ExtensionEntry.ClassLibrary, self, pCallbacks); } return control; } [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] public static void FreeInstance(void* pClassUserdata, GdExtensionClassInstancePtr pInstance) { if (pInstance.IsNull()) { return; } GdExtensionCore.MemFree(pInstance); } } 然后在加载过程中注册这个类型即可\n// ./ExtensionEntry.cs // ... [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] private static void InitializeModule(void* pUserdata, GdExtensionInitializationLevel level) { if (level != GdExtensionInitializationLevel.Scene) { return; } using StringName className = \u0026#34;AotExample\u0026#34;; using StringName parentClassName = \u0026#34;Control\u0026#34;; delegate* unmanaged[Cdecl]\u0026lt;void*, GdExtensionObjectPtr\u0026gt; createInstancePtr = \u0026amp;AotExample.CreateInstance; delegate* unmanaged[Cdecl]\u0026lt;void*, GdExtensionClassInstancePtr, void\u0026gt; freeInstancePtr = \u0026amp;AotExample.FreeInstance; GdExtensionClassCreationInfo2 classInfo = new() { IsVirtual = false, IsAbstract = false, IsExposed = true, CreateInstanceFunc = createInstancePtr, FreeInstanceFunc = freeInstancePtr, }; GdExtensionClassDb.RegisterExtensionClass2(ClassLibrary, \u0026amp;className, \u0026amp;parentClassName, \u0026amp;classInfo); } // ... \u0026lt;0x04\u0026gt; 编译与运行 publish到目标位置后，同样，需要一个.gdextension文件来说明函数入口与动态库位置\n（是publish，不是build，build的话入口符号可能不会导出）\n配置完成后，启动Godot\n出现AotExample节点即说明我们写的扩展是正常运行的\n","date":"2025-12-04T16:20:29+08:00","permalink":"/post/articles/2025/%E5%A6%82%E4%BD%95%E7%BC%96%E5%86%99gdextension%E6%8F%92%E4%BB%B6/","title":"如何编写GDExtension插件"},{"content":"国内访问一些全球的东西会有一些困难\n而Steam的部分流量会通过国内走\n比如下载和一些云存档功能\n如果规则没写好，让这些流量不走国内，会导致不必要的流量消耗与速度降低\n不同的软件需要不同的配置，某C开头软件的规则可以通过全局扩展脚本添加\nconst steamRules = [ \u0026#34;GEOSITE,category-games@cn,DIRECT\u0026#34;, \u0026#34;DOMAIN-SUFFIX,steamserver.net,DIRECT\u0026#34;, \u0026#34;DOMAIN-SUFFIX,cm.steampowered.com,DIRECT\u0026#34;, ]; function main(config, profileName) { config.rules = [...steamRules, ...config.rules]; return config; } 某V开头软件的需要添加Direct规则，然后这样写\ngeosite:category-games@cn, domain:steamserver.net, domain:cm.steampowered.com 这样就可以让Steam下载流量走直连，但让Steam社区之类的走代理\n","date":"2025-10-07T11:23:01+08:00","permalink":"/post/articles/2025/%E8%AE%A9steam%E6%B5%81%E9%87%8F%E8%B5%B0%E7%9B%B4%E8%BF%9E/","title":"让Steam流量走直连"},{"content":"使用引擎多少会需要自己搓一些工具\nGodot给出的方案是通过插件来扩展引擎的功能\n但受制于脚本的功能性，通过C#脚本来编写插件时会遇到一些问题\n（或许通过GDExtension扩展会好一点） 本文采用C#格式的API\n判断脚本类型 一般来说，插件通过GodotObject传递原始对象\n理论上我们可以通过GodotObject.GetClass()获取对象\n但由于脚本添加的自定义类型本质上是在GodotObject上附加了这个脚本\n附加的脚本不会在Godot的ClassDB中添加新的元数据\n也就是说没有改变这个GodotObject的实际类型\n这会导致GodotObject.GetClass()返回的是基类类型\n比如说下面的代码\n[GlobalClass] [Tool] public partial class TestCamera : Camera3D { // ... } 那么在插件上下文中，通过GetClass()返回的是Camera3D的类型\n这会导致在制作查看器插件或者Gizmo插件时，无法通过这种方式对类型的判断\n进而使得查看器插件的_CanHandle与Gizmo插件的_HasGizmo方法不是很好写\n还是有办法解决这个问题的\n通过比较脚本类型即可\n以_CanHandle为例\npublic partial class TestCameraInspector : EditorInspectorPlugin { // ... public override bool _CanHandle(GodotObject o) { var script = GD.Load\u0026lt;Script\u0026gt;(\u0026#34;path of script\u0026#34;); return script == o.GetScript().AsGodotObject(); } // ... } 这样就可以判断脚本类型了\n但如果说要扩展脚本功能，那么这个方法就不能判断了\n毕竟相当于附加的脚本变了\n这种情况可能直接判断基类型或者通过添加元数据判断更好\n与脚本对象交互 由于上面的问题，导致GodotObject没法直接转换为需要的脚本问题\n只能直接转换为继承基类型\n比如上面的TestCamera，在插件给的GodotObject只能强制转型到Camera3D\n那插件怎么传递数据到脚本对象呢\n只能通过动态调用的方法实现\n在GodotObject中，动态调用相关的主要有下面的方法\nGet()可以获取某个属性值 Set()可以设定某个属性值 Call()可以调用某个方法 还有其他的一些查询方法与衍生方法 动态调用是可以调用脚本方法的，只要脚本在生存期内\n在编写插件的场景中，也就是脚本要能在编辑器中运行，记得给脚本带[Tool]特性即可\n下面举个例子\n[GlobalClass] [Tool] public partial class TestCamera : Camera3D { public void TestMethod(int i) { GD.Print(i); } } public partial class TestCameraInspector : EditorInspectorPlugin { // ... // 这里给TestCamera栏添加一个按钮来触发这个TestMethod public override void _ParseCategory(GodotObject o, string category) { if (category != \u0026#34;TestCamera\u0026#34;) return; var buttonTest = new Button() { Text = \u0026#34;Test\u0026#34; }; buttonTest.ButtonUp += () =\u0026gt; {o.Call(\u0026#34;TestMethod\u0026#34;, 233)}; AddCustomControl(buttonTest); } // ... } 隐藏某个属性 有时候我们需要序列化一个属性，但希望只通过脚本内部管理\nGodot本身没有这样的设置，[Export]默认是会把这个属性放在编辑器里的\n这时候可以通过检查器插件来实现\n[GlobalClass] [Tool] public partial class TestCamera : Camera3D { [Export] public int HideProperty{ get;set; } } public partial class TestCameraInspector : EditorInspectorPlugin { // ... public override bool _ParseProperty( GodotObject @object, Variant.Type type, string name, PropertyHint hintType, string hintString, PropertyUsageFlags usageFlags, bool wide) { return name == \u0026#34;HideProperty\u0026#34;; } // ... } 这样就实现了对HideProperty的隐藏\n原理是_ParseProperty同返回值判断是否替换原有的控件\n如果返回为true，则说明需要替换\n但这里没有定义新的控件，那么就替换为空了\n","date":"2025-09-11T12:47:47+08:00","permalink":"/post/articles/2025/godot%E6%8F%92%E4%BB%B6%E5%88%B6%E4%BD%9C%E7%9A%84%E4%B8%80%E4%BA%9B%E7%A5%9E%E7%A7%98%E5%B0%8F%E6%8A%80%E5%B7%A7/","title":"Godot插件制作的一些神秘小技巧"},{"content":"有老板赞助了我一台服务器，总算是能做Gitea Action了\n（阿里99一年的性能太差了，没有做的意义）\n\u0026lt;0x01\u0026gt; Gitea的Action架构 Gitea中，Action并不是自带支持的\n这很正常，毕竟Action要部署服务，自然是需要用到容器服务的\nGitea通过act runner这个程序实现了Action的支持\n这样可以将Gitea服务器与Runner服务器分离\n而且也方便多添加几个Runner\n\u0026lt;0x02\u0026gt; 如何配置 这个act runner可以在本机运行，也可以通过Docker运行\n为了确保最佳兼容性，这边选择通过Docker运行\n首先是给服务器安装docker，这个每个系统都有自己的安装方式\n需要注意的是，不要直接apt install docker，尤其是Debian\n因为Debian库中的Docker版本很老了，很可能导致些意外情况\n建议按照官方文档安装Docker\n这里通过docker-compose的方式配置\nmkdir gitea-actrunner cd gitea-actrunner mkdir data # 如果要配置action/cache的话 nano docker-compose.yaml 配置文件如下\nversion: \u0026#34;3.8\u0026#34; services: act_runner: image: gitea/act_runner:latest container_name: act_runner restart: always environment: # 172.17.0.1是Docker容器访问宿主机的地址，3000是gitea默认端口号 - GITEA_INSTANCE_URL=http://172.17.0.1:3000 # 这里输入你的注册Token - GITEA_RUNNER_REGISTRATION_TOKEN=Your Registration token # 输入你希望给这个Runner的名字 - GITEA_RUNNER_NAME=Your runner name # 这里是建议的可用Runner标签 - GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:16-bullseye,ubuntu-22.04:docker://node:16-bullseye,ubuntu-20.04:docker://node:16-bullseye,ubuntu-18.04:docker://node:16-buster - CONFIG_FILE=/config.yaml volumes: - ./config.yaml:/config.yaml - ./data:/data - /var/run/docker.sock:/var/run/docker.sock 然后docker compose up即可启动\n如果没报错，并且gitea的注册界面出现了新的runner就是成功\n\u0026lt;0x03\u0026gt; 测试Action 创建一个仓库，用来测试action\n在仓库的.gitea/workflow/下创建一个.yaml文件\n# demo.yaml name: Gitea Actions Demo run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 on: [push] jobs: Explore-Gitea-Actions: runs-on: ubuntu-latest steps: - run: echo \u0026#34;🎉 The job was automatically triggered by a ${{ gitea.event_name }} event.\u0026#34; - run: echo \u0026#34;🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!\u0026#34; - run: echo \u0026#34;🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}.\u0026#34; - name: Check out repository code uses: actions/checkout@v4 - run: echo \u0026#34;💡 The ${{ gitea.repository }} repository has been cloned to the runner.\u0026#34; - run: echo \u0026#34;🖥️ The workflow is now ready to test your code on the runner.\u0026#34; - name: List files in the repository run: | ls ${{ gitea.workspace }} - run: echo \u0026#34;🍏 This job\u0026#39;s status is ${{ job.status }}.\u0026#34; \u0026lt;0x04\u0026gt; 解决国内获取Action脚本慢的问题 众所周知，国内访问Github有点小问题\n而上面的用到了actions/checkout@v4\n这需要从Github上下载这个checkout脚本\n有些时候，访问就很慢了\n可以在自己的gitea中把需要的脚本都Clone过来\n这有些费时费力，但确实是最稳妥的方法\n如果是快速测试能否使用Action，也可以用下面的方法\n打开config.yaml\n修改下面的选项\n# ... runner: # ... github_mirror: \u0026#39;https://gh-proxy.com/https://github.com\u0026#39; # ... # ... 这会指引act runner通过镜像clone脚本\n然后重启runner即可\n\u0026lt;0x05\u0026gt; 为什么用Docker运行 本来我是想本地运行act runner，但到这服务器的公网是NAT过来的，就行不通\n为什么呢\n首先NAT过来的公网是不能ping到自己的\n也就是说，假设服务器的ip是x.x.x.x\n那么ping x.x.x.x是没有结果的\n这就导致在注册时，runner会找不到gitea\n有人会说了，为什么不能用127.0.0.1呢\n这个也不行，这会导致容器内拉取不到gitea仓库\n因为runner是在本机上，可以通过127.0.0.1访问gitea\n但容器的127.0.0.1访问的就不是gitea了，是容器自己\n除非指定runner运行job也在本机上\n但这样又无法隔离不同的环境\n所以说，通过Docker运行act runner是兼容性最佳的方案\n","date":"2025-09-06T00:23:50+08:00","permalink":"/post/articles/2025/gitea%E4%B8%AD%E4%BD%BF%E7%94%A8action/","title":"Gitea中使用Action"},{"content":"之前不是想让Kokoro运行在C#中嘛\n但项目赶得紧，所以就先通过PythonNet嫁接过来实现\nKokoro相应还是要一点时间的，挂主线程就会阻塞\n作为CPU消耗型的任务，很自然的会想到Task.Run\n但Task.Run基于的是线程池，每次运行分配的线程是不一样的\n这会导致每次生成语音，Python上下文是不一致的\n轻则需要重新加载模型文件，重则会导致难以估计的问题\n所以需要一个后台线程来运行Python实例\n这里先给出示例代码\npublic class PythonExecutor { private readonly Thread _pythonThread; private readonly BlockingCollection\u0026lt;Func\u0026lt;object\u0026gt;\u0026gt; _tasks = new(); public PythonExecutor() { Runtime.PythonDLL = \u0026#34;Your Python Dll Path\u0026#34;; _pythonThread = new Thread(() =\u0026gt; { PythonEngine.Initialize(); PythonEngine.BeginAllowThreads(); foreach (var task in _taskQueue.GetConsumingEnumerable()) { try { using (Py.GIL()) task(); } catch (Exception ex) { Console.WriteLine($\u0026#34;Python task error: {ex}\u0026#34;); } } PythonEngine.Shutdown(); }) { IsBackground = true }; _pythonThread.Start(); } public void Task\u0026lt;T\u0026gt; EnqueueAsync\u0026lt;T\u0026gt;(Func\u0026lt;T\u0026gt; pythonFunc) { var tcs = new TaskCompletionSource\u0026lt;T\u0026gt;(); _taskQueue.Add(() =\u0026gt; { try { var result = pythonFunc(); tcs.SetResult(result); } catch (Exception ex) { tcs.SetException(ex); } return null!; }); return tcs.Task; } // 对象析构时指定无后继任务，终止线程 ~PythonExecutor() { _taskQueue.CompleteAdding(); } } 调用示例\nvar py = new PythonExecutor(); Console.WriteLine(\u0026#34;Do Heavy Calculate In Python\u0026#34;); int ans = await py.EnqueueAsync(()=\u0026gt;{ dynamic some_module = Py.Import(\u0026#34;some_module\u0026#34;); PyObject result = some_module.heavy_cal(); return result.As\u0026lt;int\u0026gt;(); }); Console.WriteLine($\u0026#34;Ans: {ans}\u0026#34;); 可通过await等待，不阻塞主线程，这样就很方便了\n","date":"2025-07-28T21:16:02+08:00","permalink":"/post/articles/2025/%E8%AE%A9pythonnet%E4%BC%98%E9%9B%85%E5%9C%B0%E8%BF%90%E8%A1%8C%E5%9C%A8%E5%8D%95%E7%8B%AC%E7%9A%84%E7%BA%BF%E7%A8%8B/","title":"让PythonNet优雅地运行在单独的线程"},{"content":"Powershell默认的样式确实不是很舒服\n但如果用oh-my-posh的话太重了，启动也慢\n有没有方法让Powershell稍微好看一点呢\n有的兄弟，有的\n可以通过Powershell的Profile来实现\n首先是打开Powershell的Profile\n可以通过下面的命令打开\nnotepad $PROFILE 第一次打开的话里面就是空的\n然后输入下面的内容\nfunction prompt { \u0026#34;[$env:COMPUTERNAME@$env:USERNAME] $($pwd.Path)`n\u0026gt; \u0026#34; } 效果如下，就变双行了\n现在看着没啥样式，分辨度还是不够\n可以通过ANSI转义序列来简单设置文字样式\nANSI转义序列通常以\\e[\u0026lt;...\u0026gt;的格式\n（Powershell中需要将反斜杠换成反引号）\n用转义序列打头，后面的文字就会应用上代码代表的样式\n比如说这样\n# 这样输出的就是绿色的233333 echo \u0026#34;`e[32m 233333\u0026#34; # 绿色的233333，红色的emmm echo \u0026#34;`e[32m 233333 `e[31m emmm\u0026#34; 下面列出一些常用的ANSI转义序列\n转义序列 含义 `e[0m 重置为默认格式 `e[1m 设置粗体 `e[3m 设置斜体 `e[4m 设置下划线 `e[9m 设置删除线 `e[30m 设置字体为黑色 `e[31m 设置字体为红色 `e[32m 设置字体为绿色 `e[33m 设置字体为黄色 `e[34m 设置字体为蓝色 `e[35m 设置字体为品红色 `e[36m 设置字体为青色 `e[37m 设置字体为白色 最后给出我的配置\nfunction prompt { \u0026#34;`e[36m[$env:COMPUTERNAME@$env:USERNAME] `e[33m$($pwd.Path)`n`e[0m\u0026gt; \u0026#34; } 效果如下，配合terminal，看着挺舒服的\n（其实研究这个主要是为了这个双行）\n","date":"2025-07-25T17:33:22+08:00","permalink":"/post/articles/2025/%E7%AE%80%E5%8D%95%E8%87%AA%E5%AE%9A%E4%B9%89powershell/","title":"简单自定义Powershell"},{"content":"\u0026lt;0x00\u0026gt; 前言 项目要有TTS，然后就找到KokoroSharp这个包 项目中的模型对英文支持很好，但中文总有种好想成为人类的感觉\n然后接着寻找，发现有新的v1.1-zh的模型\n（KokoroSharp默认使用的是v1.0的模型）\n但由于对中文的针对训练导致其他语言输出有一定劣化，所以还不是正式版\n然后就开始折腾能不能让这个项目跑起v1.1-zh的模型\n前排提示：流水账文章，而且问题也没解决\n结果是成功让v1.1-zh跑起来了，但中文输出还是很差\n所以只是做一个过程记录\n我认为可能是原本模型从torch转换到onnx导致的问题\n\u0026lt;0x01\u0026gt; 过程 尝试直接运行 如果直接加载v1.1-zh-onnx的模型，那确实是能加载的\n但如果这样运行，会有下面的错误\nusing KokoroSharp; using KokoroSharp.Core; KokoroTTS tts = KokoroTTS.LoadModel(\u0026#34;kokoro-v1.1-zh.onnx\u0026#34;); KokoroVoice heartVoice = KokoroVoiceManager.GetVoice(\u0026#34;af_heart\u0026#34;); while (true) { tts.Speak(Console.ReadLine(), heartVoice); } 输入任何文字回车后\nUnhandled exception. Microsoft.ML.OnnxRuntime.OnnxRuntimeException: [ErrorCode:InvalidArgument] Input name: \u0026#39;tokens\u0026#39; is not in the metadata\rat Microsoft.ML.OnnxRuntime.InferenceSession.LookupInputMetadata(String nodeName)\rat Microsoft.ML.OnnxRuntime.InferenceSession.LookupUtf8Names[T](IReadOnlyCollection`1 values, NameExtractor`1 nameExtractor, MetadataLookup metaLookup)\rat Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames, RunOptions options)\rat Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames)\rat Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs)\rat KokoroSharp.Core.KokoroModel.Infer(Int32[] tokens, Single[,,] voiceStyle, Single speed) in E:\\GitHub\\KokoroSharp\\KokoroSharp\\Core\\KokoroModel.cs:line 58\rat KokoroSharp.Core.KokoroJob.Progress(KokoroModel model) in E:\\GitHub\\KokoroSharp\\KokoroSharp\\Core\\KokoroJob.cs:line 28\rat KokoroSharp.Core.KokoroEngine.\u0026lt;.ctor\u0026gt;b__2_0() in E:\\GitHub\\KokoroSharp\\KokoroSharp\\Core\\KokoroEngine.cs:line 20 会说在元数据中找不到tokens\n修改入口名 运行一个模型其实跟调用一个函数差不多\n首先要确定的是这个模型的输入参数有什么\n知道了区别后，就可以尝试修改入口名了\n这里使用python修改这个入口名\nimport onnx def ModEntryName(onnxModel: onnx.ModelProto, oldName: str, newName: str) -\u0026gt; onnx.ModelProto: # 查找输入节点并修改 for input_tensor in onnxModel.graph.input: if input_tensor.name == oldName: input_tensor.name = newName # 遍历修改后面的每个节点名 for node in onnxModel.graph.node: for i, name in enumerate(node.input): if name == oldName: node.input[i] = newName return onnxModel model_path = \u0026#34;kokoro-v1.1-zh\u0026#34; model = onnx.load(model_path + \u0026#34;.onnx\u0026#34;) ModEntryName(model, \u0026#34;input_ids\u0026#34;, \u0026#34;tokens\u0026#34;) onnx.save(model, model_path + \u0026#34;-mod.onnx\u0026#34;) 修改入口参数类型 像上面改好后，再次运行\n还是有报错，如下\nUnhandled exception. Microsoft.ML.OnnxRuntime.OnnxRuntimeException: [ErrorCode:InvalidArgument] Tensor element data type discovered: Float metadata expected: Int32\rat Microsoft.ML.OnnxRuntime.ManagedTypeProjection.CreateTensorProjection(NamedOnnxValue node, NodeMetadata elementMeta)\rat Microsoft.ML.OnnxRuntime.ManagedTypeProjection.CreateProjection(NamedOnnxValue namedOnnxValue, NodeMetadata metadata)\rat Microsoft.ML.OnnxRuntime.NamedOnnxValue.InputToOrtValueHandle(NodeMetadata metadata, IDisposable\u0026amp; memoryOwner)\rat Microsoft.ML.OnnxRuntime.InferenceSession.ExtractOrtValueHandleForInput(NamedOnnxValue input, NodeMetadata metadata, IDisposable\u0026amp; memOwner)\rat Microsoft.ML.OnnxRuntime.InferenceSession.GetOrtValuesHandles(IReadOnlyCollection`1 values, MetadataLookup metaLookup, OrtValueHandleExtractor ortValueExtractor, DisposableArray`1\u0026amp; disposer)\rat Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames, RunOptions options)\rat Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs, IReadOnlyCollection`1 outputNames)\rat Microsoft.ML.OnnxRuntime.InferenceSession.Run(IReadOnlyCollection`1 inputs)\rat KokoroSharp.Core.KokoroModel.Infer(Int32[] tokens, Single[,,] voiceStyle, Single speed) in E:\\GitHub\\KokoroSharp\\KokoroSharp\\Core\\KokoroModel.cs:line 58\rat KokoroSharp.Core.KokoroJob.Progress(KokoroModel model) in E:\\GitHub\\KokoroSharp\\KokoroSharp\\Core\\KokoroJob.cs:line 28\rat KokoroSharp.Core.KokoroEngine.\u0026lt;.ctor\u0026gt;b__2_0() in E:\\GitHub\\KokoroSharp\\KokoroSharp\\Core\\KokoroEngine.cs:line 20 参数错误说是\n这时候需要通过下面的代码来检查模型的参数类型\nusing Microsoft.ML.OnnxRuntime; // 原本使用的v1.0模型 using var kokoroV1 = new InferenceSession(\u0026#34;kokoro.onnx\u0026#34;); // 现在打算使用的v1.1-zh模型 using var kokoroV1_1 = new InferenceSession(\u0026#34;kokoro-v1.1-zh-mod.onnx\u0026#34;); Console.WriteLine(\u0026#34;Kokoro v1.0\u0026#34;); foreach (var kvp in kokoroV1.InputMetadata) { var name = kvp.Key; var meta = kvp.Value; Console.WriteLine($\u0026#34;name: {name}\u0026#34;); Console.WriteLine($\u0026#34;type: {meta.ElementType}\u0026#34;); } Console.WriteLine(); Console.WriteLine(\u0026#34;Kokoro v1.1-zh\u0026#34;); foreach (var kvp in kokoroV1_1.InputMetadata) { var name = kvp.Key; var meta = kvp.Value; Console.WriteLine($\u0026#34;name: {name}\u0026#34;); Console.WriteLine($\u0026#34;type: {meta.ElementType}\u0026#34;); } 输出如下\nKokoro v1.0\rname: tokens\rtype: System.Int64\rname: style\rtype: System.Single\rname: speed\rtype: System.Single\rKokoro v1.1-zh\rname: tokens\rtype: System.Int64\rname: style\rtype: System.Single\rname: speed\rtype: System.Int32 可以看到speed参数类型不同，需要修改下\n这里用python改入口参数的类型\nimport onnx from onnx import helper, TensorProto def ModEntryType(onnxModel: onnx.ModelProto, entryName: str, targetType) -\u0026gt; onnx.ModelProto: graph = onnxModel.graph for i, input_tensor in enumerate(graph.input): if input_tensor.name == entryName: # 获取原始维度 shape = [ dim.dim_value if dim.HasField(\u0026#34;dim_value\u0026#34;) else -1 for dim in input_tensor.type.tensor_type.shape.dim ] new_input = helper.make_tensor_value_info( name=input_tensor.name, elem_type=targetType, shape=shape ) # 替换旧输入 graph.input.remove(input_tensor) graph.input.insert(i, new_input) onnx.checker.check_model(onnxModel) return onnxModel model_path = \u0026#34;kokoro-v1.1-zh\u0026#34; model = onnx.load(model_path + \u0026#34;.onnx\u0026#34;) ModEntryType(model, \u0026#34;tokens\u0026#34;, TensorProto.FLOAT) onnx.save(model, model_path + \u0026#34;-mod.onnx\u0026#34;) 然后替换模型文件\n获取新voice 现在模型是可以跑了，但输出的语音还是英文\n参考v1.0的文档，可以知道示例代码中的af_heart表示一种语音\n所以我们要找到中文的语音叫什么\n而又参考v1.1-zh的python示例，可以知道示例中传入的是zf_001\n但直接这样传入会报这样的错误\nUnhandled exception. System.InvalidOperationException: Sequence contains no matching element\rat System.Linq.ThrowHelper.ThrowNoMatchException()\rat System.Linq.Enumerable.First[TSource](IEnumerable`1 source, Func`2 predicate)\rat KokoroSharp.KokoroVoiceManager.GetVoice(String name) in E:\\GitHub\\KokoroSharp\\KokoroSharp\\HighLevel\\KokoroVoiceManager.cs:line 33\rat Program.\u0026lt;Main\u0026gt;$(String[] args) in E:\\Code\\CSharp\\RiderTest\\Program.cs:line 34 就是说找不到zf_001这个元素\n通过反编译，可以知道KokoroSharp是通过检索./voices文件夹来确定的\n这个文件夹下面都是*.npy类型的文件\n比如说之前的af_heart，就有af_heart.npy文件\n那zf_001的哪里找呢\n在v1.1-zh-onnx的huggingface上，还有voice文件夹\n里面就是这些voice文件了，但都是bin格式的，还需要做一个转换\n代码如下\nimport numpy as np bin_path = \u0026#34;zf_001.bin\u0026#34; data = np.fromfile(bin_path, dtype=np.float32) data = data.reshape((510, 1, 256)) np.save(\u0026#34;zf_001.npy\u0026#34;, data) 然后把输出文件添加到运行目录中的./voices中就可以了\n（也就是在项目中添加一个构建时复制的文件）\n现在再运行下面的代码\nusing KokoroSharp; using KokoroSharp.Core; // 经过参数名称与类型修改的模型文件 KokoroTTS tts = KokoroTTS.LoadModel(\u0026#34;kokoro-v1.1-zh-mod.onnx\u0026#34;); KokoroVoice testVoice = KokoroVoiceManager.GetVoice(\u0026#34;zf_001\u0026#34;); while (true) { tts.Speak(Console.ReadLine(), testVoice); } 总算是能跑了，但效果还是不大能满意\n\u0026lt;0x02\u0026gt; 其他尝试 修改Tokenizer 我后面发现新的模型的tokenizer.json有变动\n（v1.0版本的config.json，v1.1-zh版本的config.json）\n于是我就想把这些应用上\n但KokoroSharp的Tokenizer将这些硬编码到代码中\n所以我只能把代码clone下来修改\n修改后只能说差别不大\n尝试pull request中的修改 参考这个pull request修改了代码\n也是差别不大\n\u0026lt;0x03\u0026gt; 没办法 最后实在没办法，找到用onnx跑这个模型的python代码\n跑了一下，一听，您猜怎么着\n跑出来效果跟我用C#跑的差不多烂\n所以我觉得是torch转换到onnx导致效果没那么好\n比起之前的话更像是不会中文的人说中文，也算有点效果\n","date":"2025-07-19T02:48:59+08:00","permalink":"/post/articles/2025/%E8%AE%A9kokorosharp%E7%94%A8%E4%B8%8Av1.1-zh%E7%9A%84%E6%A8%A1%E5%9E%8B/","title":"让KokoroSharp用上V1.1-zh的模型"},{"content":"做毕业设计时要用到一个库\n然后发现出现了一个没见过的类型IAsyncEnumerable\n看着就跟异步有关，但直接await又直接报错\n于是小研究了一下，感觉这玩意还挺有用的\n\u0026lt;0x01\u0026gt; 什么是IAsyncEnumerable 从字面意思翻译，就是可枚举的异步\n按官方的说法，这个是异步流\n这是.net8.0加入的新的异步类型\n可以实现流式异步\n\u0026lt;0x02\u0026gt; 有什么用 这个类型的迭代器可以使用async/await关键字\n实现迭代器中的异步\npublic static Task\u0026lt;string\u0026gt; IOBindingTask() { // 需要等待IO的任务 // 返回string } // 异步流迭代器 public static async IAsyncEnumerable\u0026lt;string\u0026gt; GetDataAsync() { yield return await IOBindingTask(); yield return await IOBindingTask(); yield return await IOBindingTask(); } // 使用迭代器 public static async void UseData() { await foreach(var d in GetDataAsync()) { Console.WriteLine(d); } } 这里给出一个例子\n比如说我们需要从一个在线的API中获取一系列数据\n长度不定，并且次返回数据都需要一点时间\n按以前可以是这么写\npublic static Task\u0026lt;string\u0026gt;? GetData() { // 获取数据的方法，数据结尾返回null类型Task // ... } public static IEnumerable\u0026lt;Task\u0026lt;string\u0026gt;\u0026gt; GetDataEnumerable() { var t = GetData(); while(t != null) { yield return t; t = GetData();\t} } public async void ProcessData() { // ... foreach(Task\u0026lt;string\u0026gt; t in GetDataEnumerable()) { string data = await t; // ... } // ... } 通过IAsyncEnumerable就可以这么写\npublic static Task\u0026lt;string\u0026gt; GetData() { // 获取数据的方法，数据结尾返回空字符串 // ... } public static async IAsyncEnumerable\u0026lt;string\u0026gt; GetFromWeb() { while(true) { var d = await GetData(); if(d == string.Empty) { yield break; } yield return d; } } public async void ProcessData() { // ... await foreach(var data in GetFromWeb()) { // 处理data } // ... } 通过IAsyncEnumerable可以更关注数据的处理，而不需要关心循环的问题\n并且将异步等待放在迭代器中，这样调用时只需要处理最后的数据就可以\n目前看这个异步流更像是语法糖，让异步获取多组数据更加优雅\n之后有时间再研究研究，看看有没有别的细节不同\n","date":"2025-07-08T19:41:12+08:00","permalink":"/post/articles/2025/csharp%E4%B8%AD%E7%9A%84iasyncenumerable%E5%BC%82%E6%AD%A5%E6%B5%81/","title":"C#中的IAsyncEnumerable异步流"},{"content":"Json序列化是项目开发中非常常见的需求\n（现在那么多的API靠Json传递参数之类的）\n而C#对Json序列化/反序列化的支持也是非常完善的\n下面讲几个我在项目中使用到的技巧\n需要注意的是，下面的代码均使用.net9以上，老版本的.net可能还不支持相关功能\nJson序列化/反序列化主要通过添加Attribute或者设置option实现\n\u0026lt;0x01\u0026gt; 通过Attribute可以实现的 属性值重命名 一般情况下，Json序列化/反系列化基于属性名\n大部分情况下也没啥问题\n但有时，Json属性名会和C#关键字冲突\n比方这个Json\n{ \u0026#34;object\u0026#34;: \u0026#34;list\u0026#34;, \u0026#34;data\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;deepseek-chat\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;model\u0026#34;, \u0026#34;owned_by\u0026#34;: \u0026#34;deepseek\u0026#34; }, { \u0026#34;id\u0026#34;: \u0026#34;deepseek-reasoner\u0026#34;, \u0026#34;object\u0026#34;: \u0026#34;model\u0026#34;, \u0026#34;owned_by\u0026#34;: \u0026#34;deepseek\u0026#34; } ] } （Deepseek获取模型的json示例）\n这时候就必须重命名了\n可以通过JsonPropertyName特性给属性重命名，匹配Json的名字\npublic class ModelsObject { [JsonPropertyName(\u0026#34;object\u0026#34;)] public string ObjectType { get; set; } public ModelData[] data { get; set; } public class ModelData { public string id { get; set; } [JsonPropertyName(\u0026#34;object\u0026#34;)] public string ObjectType { get; set; } public string owned_by { get; set; } } } 这里就给object这个属性做了重命名\n通常Json都是下划线命名，C#属性通常大驼峰命名\n所以我基本上是都做重命名的\n枚举重命名 跟上面差不多，Json的命名习惯跟C#有差异\n我也习惯自己做一遍重命名\n需要使用JsonStringEnumMemberName特性\npublic enum EnumTest { [JsonStringEnumMemberName(\u0026#34;enum_a\u0026#34;)] AAA, [JsonStringEnumMemberName(\u0026#34;enum_b\u0026#34;)] BBB, } 是否包含某属性 一般情况下C#的序列化会去序列化public属性，不序列化private/protected属性\n这个是可以控制的，通过标记JsonInclude特性或者JsonIgnore特性\npublic class TestObject { // 这个属性按默认是会被序列化的，但这里指定忽略 [JsonIgnore] public string PropertyA { get; set; } // 这个属性默认是不会被序列化的，但这里指定包含 [JsonInclude] private string PropertyB { get; set; } } 多态类型的序列化 有些时候，会需要传一个List，List里面有不同类型的数据\n理想的情况就是List指定基类类型，里面存放派生类型\n这时候要给这个List做序列化，需要用JsonDerivedType特性做下派生类型标记\n不然只会序列化基类的属性，即使List中的对象是多态的\nvar objectList = new List\u0026lt;ObjectBase\u0026gt;(); // 假设随便塞了点数据 var jsonString = JsonSerializer.Serialize(objectList); [JsonDerivedType(typeof(ObjectA))] [JsonDerivedType(typeof(ObjectB))] public class ObjectBase { public string Name { get; set; } } public class ObjectA : ObjectBase { public int IntProperty { get; set; } } public class ObjectB : ObjectBase { public float FloatProperty { get; set; } } \u0026lt;0x02\u0026gt; 通过option可以实现的 这里的option指的是JsonSerializerOptions类型对象\n可以在序列化时传入的参数\n比如这样\nvar option = new JsonSerializerOptions() { //... } var jsonString = JsonSerializer.Serialize(obj, option); 忽略/包括空值属性 C#在处理空值时，默认会带上这个属性，输出为null\n可以设置成如果是空值，就忽略这个属性\n需要通过设置DefaultIgnoreCondition\n比如说\n// 当属性值为空时忽略这个属性输出 var option = new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, } JsonIgnoreCondition有下面的值\nNever 总是序列化/反序列化，并且无视IgnoreNullValues的设置 Always 总是忽略 WhenWritingDefault 当属性值等于默认值时忽略 WhenWritingNull 当属性值为null时忽略 只有类型为引用类型时有效 从枚举中转换 很多时候，Json传递的string值是固定的几个选项\n这时候，在C#中选择枚举是非常合适的\n需要在设置中指定Converters\n比如这样\n// 这样指定即可 var option = new JsonSerializerOptions() { Converters = { new JsonStringEnumConverter() }, } ","date":"2025-06-28T15:55:52+08:00","permalink":"/post/articles/2025/csharp%E4%B8%ADjson%E5%BA%8F%E5%88%97%E5%8C%96%E5%B0%8F%E6%8A%80%E5%B7%A7/","title":"C#中Json序列化小技巧"},{"content":"有些时候我们希望让三维物体在一个面某方向上可以看到，另一方向不能看到\n就像这样\n这时候需要写一个shader来实现这个功能\n\u0026lt;0x01\u0026gt; 原理 既然是某个面一侧可以显示，一侧不能显示\n我们可以联想到判断面正反的量——法向\n所谓法向是一个垂直于平面的向量，向量正方向表示平面正面\n而我们知道，通过向量乘法，我们可以判断俩向量夹角是否小于90°\n通过俩向量得出的值即可判断渲染的部分在面的正侧还是背侧\n\u0026lt;0x02\u0026gt; 实现 这里通过Unity Shader Graph实现\n通过Shader Graph可以很方便地继承URP自带的那几个Shader的效果\n比如默认的Lit Shader\n首先先新建一个Shader Graph\n选择Lit Shader Graph 在Shader Graph中创建三个参数\n在Graph Inspector的Graph Settings中打开Alpha Clipping，然后这样连接\n这里暴露3个参数\nPlanePosition表示平面的位置\nPlaneNormal表示平面法向方向\nBaseMap表示物体的材质\n平面的参数需要通过脚本传入，所以要新建一个脚本，代码如下\nusing UnityEngine; // 让脚本可以在编辑模式中运行 [ExecuteInEditMode] public class PlaneCutShaderSetter : MonoBehaviour { public Transform plane; private static readonly int PlanePositionID = Shader.PropertyToID(\u0026#34;_PlanePosition\u0026#34;); private static readonly int PlaneNormalID = Shader.PropertyToID(\u0026#34;_PlaneNormal\u0026#34;); private Renderer _renderer; private Material _material; // 通过Awake，使得添加脚本时自动更改材质 private void Awake() { _renderer = GetComponent\u0026lt;Renderer\u0026gt;(); _renderer.material = new Material(Shader.Find(\u0026#34;Shader Graphs/PlaneCutShader\u0026#34;)); _material = _renderer.sharedMaterial; } private void Update() { if (!plane) return; _material.SetVector(PlanePositionID, plane.position); _material.SetVector(PlaneNormalID, plane.up); } // 通过OnDestroy，使得销毁组件时更改材质为默认材质 private void OnDestroy() { _renderer.material = new Material(Shader.Find(\u0026#34;Universal Render Pipeline/Lit\u0026#34;)); } } \u0026lt;0x03\u0026gt; 测试 在场景中添加一个物体和一个平面\n然后在物体上添加上面写的脚本组件\n在组件中指定平面\n效果如下\n这就实现了简单的平面截断效果\n\u0026lt;0x04\u0026gt; 优化 不指定平面情况 目前的实现是要求给一个平面，如果不给的话显示会有问题\n如果需要不指定平面就正常显示的话有两个思路\n首先是给PlaneNormal和PlanePosition设定默认值\n比如说PlanePosition设定默认值给一个很低的值，然后给PlaneNormal设定(0,1,0)\n也可以修改Shader Graph，添加新的参数控制是否进行截断处理\n然后修改脚本代码，添加不指定平面的情况\nusing UnityEngine; [ExecuteInEditMode] public class PlaneCutShaderSetter : MonoBehaviour { public Transform plane; private static readonly int CutEnableID = Shader.PropertyToID(\u0026#34;_CutEnable\u0026#34;); private static readonly int PlanePositionID = Shader.PropertyToID(\u0026#34;_PlanePosition\u0026#34;); private static readonly int PlaneNormalID = Shader.PropertyToID(\u0026#34;_PlaneNormal\u0026#34;); private Renderer _renderer; private Material _material; private void Awake() { _renderer = GetComponent\u0026lt;Renderer\u0026gt;(); _renderer.material = new Material(Shader.Find(\u0026#34;Shader Graphs/PlaneCutShader\u0026#34;)); _material = _renderer.sharedMaterial; } private void Update() { if (plane) { _material.SetFloat(CutEnableID, 1); _material.SetVector(PlanePositionID, plane.position); _material.SetVector(PlaneNormalID, plane.up); } else { _material.SetFloat(CutEnableID, 0); } } private void OnDestroy() { _renderer.material = new Material(Shader.Find(\u0026#34;Universal Render Pipeline/Lit\u0026#34;)); } } 需要指定多个平面的情况 没啥特别好的办法，就是在Shader Graph中多复制几个平面判断\n然后在脚本中传参\n（因为我没找到办法实现数组类型传参）\n","date":"2025-05-23T00:57:35+08:00","permalink":"/post/articles/2025/unity%E4%B8%AD%E5%AE%9E%E7%8E%B0%E5%B9%B3%E9%9D%A2%E6%88%AA%E6%96%AD%E7%9A%84%E6%95%88%E6%9E%9C/","title":"Unity中实现平面截断的效果"},{"content":"最近在做一个项目\n需要做版本管理，并且游戏项目嘛，二进制文件很多\nGithub免费LFS额度根本不够用，所以我搭了个Gitea服务器\n然后就有很神秘的问题\n我在从主分支fork的分支上做更改\n然后每次同步主分支进度后本地pull时，会报LFS找不到目标文件\n之前反正仓库不大，干脆重新clone了，这样也不是事\n其实需要在Gitea中手动同步一下\n（我目前不知道为啥这么设计）\n找到fork的仓库，然后点击设置\n设置下面有个LFS界面，有一个查找指针文件的选项\n点击这个选项后，可以检查这个仓库LFS指针情况\n会有一个选项让Gitea自动寻找，点击就可以了 （关联可访问的OID）\n然后在本地pull就不会报404了\n我也不清楚Github之类的平台对LFS的支持情况，没用过\n（之前真的只用git同步代码的）\n","date":"2025-05-04T17:47:53+08:00","permalink":"/post/articles/2025/gitea%E4%B8%AD%E4%BD%BF%E7%94%A8lfs%E5%8F%AF%E8%83%BD%E9%81%87%E5%88%B0%E7%9A%84404%E9%97%AE%E9%A2%98/","title":"Gitea中使用LFS可能遇到的404问题"},{"content":"首先，为什么要学习自定义Editor\n一个引擎自带的工具永远不能涵盖所有工程中的问题\n在一个游戏项目中，开发者不仅需要开发游戏本身，还需要开发一系列工具来辅助游戏开发\n就好比厨子做菜还要磨磨刀\n在Godot中，开发自定义界面是容易很多的\n界面开发流程上跟开发游戏UI流程差不多\n本篇采用Godot-mono4.4，也就是用C#开发自定义界面\n一步一步，实现一个简单的Markdown查看器\n为啥实现一个Markdown查看器呢，因为这个足够简单\n而且能把开发Editor中的大部分逻辑都过一遍\n\u0026lt;0x01\u0026gt; 新建一个插件 在Godot中，我们需要在插件中实现自定义界面\n在项目 \u0026gt;\u0026gt; 项目设置 \u0026gt;\u0026gt; 插件中选择创建新插件\n需要注意的是，这里的信息最好后面就别改了\n可能是由于C#的编译问题，如果后面改了会导致无法启用插件\n并且由于C#是编译型的语言，每次测试插件都需要编译项目后重新启用\n然后Godot就会生成插件需要的文件，里面包含这样的一个C#脚本\n// ViewMain.cs #if TOOLS using Godot; using System; [Tool] public partial class ViewMain : EditorPlugin { public override void _EnterTree() { // Initialization of the plugin goes here. } public override void _ExitTree() { // Clean-up of the plugin goes here. } } #endif 其中，[Tool]表示这个脚本可以在编辑器中运行\n_EnterTree()表示启用插件时的逻辑，_ExitTree()表示禁用插件时的逻辑\n\u0026lt;0x02\u0026gt; 在场景中编辑界面 Godot的自定义界面需要自己定义一个场景，开发逻辑跟游戏中界面一样\n首先创建一个用户界面场景，也就是以Control节点为根节点的场景\n保存到插件的目录下，这里保存在res://addons/MarkdownViewer/window.tscn\n然后在代码中添加实例化界面的代码\n// MainView.cs #if TOOLS using Godot; using System; [Tool] public partial class ViewMain : EditorPlugin { private Control _dock; // 让别的脚本可以访问到根场景节点 public static Control RootNode; public override void _EnterTree() { _dock = GD.Load\u0026lt;PackedScene\u0026gt;(\u0026#34;res://addons/MarkdownViewer/window.tscn\u0026#34;).Instantiate\u0026lt;Control\u0026gt;(); AddControlToDock(DockSlot.RightBl, _dock); RootNode = _dock; } public override void _ExitTree() { RemoveControlFromDocks(_dock); _dock.Free(); } } #endif 如果有接触过其他的前端开发框架，那应该是挺好理解Godot的控件开发的\n在编辑器中编辑这个场景，加入一些需要的控件\n（为了方便起见节点名也是类型名）\nControl/VBoxContainer/HBoxContainer/Button这个按钮是用来选择文件的\nControl/VBoxContainer/HBoxContainer/Lable这个是用来显示当前选择文件的\nControl/VBoxContainer/ScrollContainer这个是显示Markdown内容的视图\nControl/FileDialog是用来做选择文件交互的窗口，也是一个自带的控件\n给控件添加脚本 给控件添加脚本跟开发游戏中界面类似，但要注意加上[Tool]\n前面也讲到，这个特性表示这个脚本可以运行在编辑器里\n比如说这里给选择文件的按钮加上脚本\n实现点击选择文件的功能\n选择Button节点，右键，添加脚本，给脚本起个名字\n这里叫FileButton，里面代码如下\n// FileButton.cs using Godot; using System; [Tool] public partial class FileButton : Button { public override void _Ready() { ButtonDown += () =\u0026gt; ViewMain.RootNode.GetNode\u0026lt;FileDialog\u0026gt;(\u0026#34;FileDialog\u0026#34;).Show(); } } 修改完脚本后，记得保存场景\n因为用的是C#，所以每次测试插件都要重新构建并重新启用\n按同样的方法，给FileDialog添加一个脚本，做选择文件后的回调\n// MarkdownDialog.cs using System; [Tool] public partial class MarkdownDialog : FileDialog { public override void _Ready() { FileSelected += (s) =\u0026gt; ViewMain .RootNode .GetNode\u0026lt;Label\u0026gt;(\u0026#34;VBoxContainer/HBoxContainer/Label\u0026#34;) .Text = s; } } \u0026lt;0x03\u0026gt; 完成简单的解析逻辑 这里主要就是演示，所以解析逻辑非常简单\n只做基本的文字和图片的解析逻辑\n这里用一个类型表示Markdown的内容\n创建一个新的C#脚本，代码如下\n//MarkdownElement.cs using Godot; public abstract class MarkdownElement { protected string OriginCode { get; set; } public abstract Control GetControl(); } public class TextElement : MarkdownElement { public string Text { get; set; } public override Control GetControl() { return new Label { Text = Text }; } } public class ImageElement : MarkdownElement { public string ImagePath { get; set; } public override Control GetControl() { var image = Image.LoadFromFile(ImagePath); return new TextureRect() { Texture = ImageTexture.CreateFromImage(image), ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize, StretchMode = TextureRect.StretchModeEnum.KeepAspect, CustomMinimumSize = new Vector2(0f, 200f), }; } } 还需要一个新的工具类，负责解析Markdown文档内容\n代码如下\n//MarkdownObject.cs using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using Godot; public class MarkdownObject { public List\u0026lt;MarkdownElement\u0026gt; elements = []; private string _filePath; private const string ImagePattern = @\u0026#34;\\((.*?)\\)\u0026#34;; public void GenerateElements(string filePath) { var localPath = ProjectSettings.GlobalizePath(filePath); _filePath = localPath; elements.Clear(); foreach (var line in File.ReadLines(localPath)) { MarkdownElement element; if (line.StartsWith(\u0026#39;!\u0026#39;)) { string path = Regex.Match(line, ImagePattern).Groups[1].Value; element = new ImageElement() { ImagePath = $\u0026#34;res://{path}\u0026#34;, }; } else { element = new TextElement { Text = line }; } elements.Add(element); } } } 在运行时添加控件 在Godot中，只需要在运行时动态添加节点即可\n这里就写在文件选择事件里面\n// MarkdownDialog.cs using System; [Tool] public partial class MarkdownDialog : FileDialog { public override void _Ready() { FileSelected += (s) =\u0026gt; ViewMain .RootNode .GetNode\u0026lt;Label\u0026gt;(\u0026#34;VBoxContainer/HBoxContainer/Label\u0026#34;) .Text = s; FileSelected += (s) =\u0026gt; { var vbox = ViewMain.RootNode.GetNode\u0026lt;VBoxContainer\u0026gt;(\u0026#34;VBoxContainer/ScrollContainer/VBoxContainer\u0026#34;); foreach (Node child in vbox.GetChildren()) { vbox.RemoveChild(child); child.QueueFree(); } MarkdownObject obj = new MarkdownObject(); obj.GenerateElements(s); foreach (var element in obj.elements) { vbox.AddChild(element.GetControl()); } }; } } 创建一个简单的Markdown文档，然后在插件窗口中选择这个文档\n正常情况下得到的显示是这样的\n到目前为止，我们已经实现了简单的Markdown查看器\n虽然比较简陋，但也算简单带过插件开发了\n","date":"2025-04-15T10:49:45+08:00","permalink":"/post/articles/2025/godot%E4%B8%AD%E8%87%AA%E5%AE%9A%E4%B9%89editor/","title":"Godot中自定义Editor"},{"content":"\u0026lt;0x01\u0026gt; 任务说明 缓冲区高速缓存 可扩展大小的文件 子目录 同步 \u0026lt;0x02\u0026gt; 代码实现 缓冲区高速缓存 首先我们要确定下我们要缓存什么东西\npintos中，文件在硬盘中按块存储，块大小512字节\n每个块有一个块号，可以用这个块号访问到硬盘中的数据块\n那我们的缓存其实是让系统先别去读写硬盘\n先在缓冲中读写，然后在合适的时机再写入到硬盘中\n为什么这么做呢，软件运行过程中可能会对同一个文件多次读写\n如果访问硬盘的话每次都要等硬盘响应，效率不行\n如果是缓存的话，访问的就是内存了，快多了\n首先先声明一个缓存对象与缓存本身\nstruct cache_buffer { char data[BLOCK_SECTOR_SIZE]; }; static struct cache_buffer cache[NUM_SECTORS]; // cache本身 // 缓存块类型 struct cache_entry { block_sector_t sector; // 硬盘块号 size_t index; // 缓存标号 struct condition queue; // 对于多线程读写的同步控制 struct hash_elem elem; // 使用hash数据结构 bool dirty; // 有无修改 }; 对于这个数据结构，我们采用控制块与数据块分离的方式\n这样方便内存对齐\n（cache_buffer的东西放在cache_entry里面也是可以的）\n这里采用的缓存算法是clock算法，并带有两个标识位\n使用pintos自带的bitmap类型表示所有64个块的标识符\n（一样，这个东西放在cache_entry也是可以的）\n然后考虑检索效率，使用hashmap作为检索结构\n如果使用list，虽然也可以通过遍历的方式检索\n但考虑到缓存时常可能没缓存到需要的块，这时候需要遍历整个链表，效率不高\n整体声明如下\nstruct cache_buffer { char data[BLOCK_SECTOR_SIZE]; }; static size_t clock_index; // clock算法的index static struct cache_buffer cache[NUM_SECTORS]; // cache本身 static struct cache_entry *entries[NUM_SECTORS]; // 缓存的数据结构列表 static struct bitmap *refbits; // 是否被访问 static struct bitmap *usebits; // 是否被修改 static struct hash cache_hashmap; // cache的hashmap static struct lock cache_lock; // cache锁 static struct condition cache_cond; // 如果所有的缓存块都有使用就阻塞 下面是clock算法的实现\n// 寻找是否在缓存中，如果没有，就用clock算法替换一块 static bool cache_find_entry(block_sector_t sector, struct cache_entry **entry) { struct cache_entry *e = malloc(sizeof(struct cache_entry)); e-\u0026gt;sector = sector; // 如果所有的缓存块都在等 while (bitmap_all(usebits, 0, NUM_SECTORS)) { cond_wait(\u0026amp;cache_cond, \u0026amp;cache_lock); } struct hash_elem *found = hash_insert(\u0026amp;cache_hashmap, \u0026amp;e-\u0026gt;elem); if (found == NULL) { // clock算法循环 while (bitmap_test(refbits, clock_index) || bitmap_test(usebits, clock_index)) { bitmap_reset(refbits, clock_index); clock_index = (clock_index + 1) % NUM_SECTORS; } // 释放旧块 struct cache_entry *old_entry = entries[clock_index]; if (old_entry != NULL) { block_write(fs_device, old_entry-\u0026gt;sector, \u0026amp;cache[old_entry-\u0026gt;index]); hash_delete(\u0026amp;cache_hashmap, \u0026amp;old_entry-\u0026gt;elem); free(old_entry); } // 构建新块 cond_init(\u0026amp;e-\u0026gt;queue); e-\u0026gt;dirty = false; e-\u0026gt;index = clock_index; entries[e-\u0026gt;index] = e; clock_index = (clock_index + 1) % NUM_SECTORS; } else { free(e); e = hash_entry(found, struct cache_entry, elem); while (bitmap_test(usebits, e-\u0026gt;index)) { cond_wait(\u0026amp;e-\u0026gt;queue, \u0026amp;cache_lock); } } // 标记新块被使用 bitmap_mark(usebits, e-\u0026gt;index); *entry = e; return (found != NULL); } 然后是同步相关的问题（后面就不写了）\n可以看到上面的代码已经包含了所有缓存块都使用的情况和单独缓存块使用的情况\n对于所有缓存块都使用的情况，那么线程就必须等待有缓存块释放\n对于单个缓存块使用的情况，其实等同于单个硬盘快被多个线程使用\n这时候也是等待\n在buffer_cache_release中，会释放对应的锁\n最后是需要定期得清理缓存，这里直接新建一个线程，差不多隔1.5秒执行一次\n（我这里设定TIMER_FREQ为1000）\n所谓清理就是把缓存中使用过得，有数据就写到硬盘里\nthread_create(\u0026#34;cache-sync\u0026#34;, PRI_MAX, cache_write_thread_func, NULL); // 刷新所有的缓存块 void buffer_cache_flush(void) { lock_acquire(\u0026amp;cache_lock); for (size_t i = 0; i \u0026lt; NUM_SECTORS; i++) { // 同步缓存中脏块的更改 if (entries[i] != NULL \u0026amp;\u0026amp; entries[i]-\u0026gt;dirty \u0026amp;\u0026amp; !bitmap_test(usebits, i)) { block_write(fs_device, entries[i]-\u0026gt;sector, \u0026amp;cache[i]); entries[i]-\u0026gt;dirty = false; } } lock_release(\u0026amp;cache_lock); } // 清理缓存块的线程函数 void cache_write_thread_func(void *aux UNUSED) { while (true) { timer_sleep(1500); buffer_cache_flush(); } } 然后别忘了在filesys_done里面最后保存一次缓存数据\nvoid filesys_done(void) { // 关闭前强制同步下缓存的内容 buffer_cache_flush(); free_map_close(); } 其他的代码都是简单的转换和处理，这里就不写明了\n具体代码看仓库吧\n可扩展的文件大小 首先我们看到inode_disk的部分\n这个是硬盘数据块的抽象\nstruct inode_disk { block_sector_t start; /* First data sector. */ off_t length; /* File size in bytes. */ unsigned magic; /* Magic number. */ uint32_t unused[125]; /* Not used. */ }; 可以看到就一个数据起始点，然后一个文件大小，剩下的都没啥用\n通过学习操作系统\n我们知道，现代操作系统的文件系统喜欢搞什么直接引用块，间接引用块之类的\n这就是我们要实现的东西\n首先我们先把这个类改造成这样\nstruct inode_disk { block_sector_t parent_dir; // 指向父目录（不是这步的东西） bool is_dir; // 是否为路径（不是这步的东西） off_t length; // 文件大小，字节 block_sector_t direct[122]; // 直接寻址块 block_sector_t indirect; // 一层间接 block_sector_t doubly_indirect; // 二层间接 unsigned magic; /* Magic number. */ }; 这里需要注意，block_sector_t部分不能超过126个单元\n因为硬盘的扇区是512字节，block_sector_t是4字节\noff_t也是4字节，然后神秘magic数字也是4字节\n这样的话刚好512字节\n然后声明下这两个类型，作为一次间接和二次间接\nstruct indirect_block { block_sector_t direct[128]; }; struct doubly_indirect_block { block_sector_t indirect_block[128]; }; 后面就是想办法分配和回收这些东西了\n这里别的就不讲了，主要讲文件大小扩展的事情\n// 扩展下文件长度 static bool extend_inode_length(struct inode_disk *inode, off_t length) { ASSERT(inode != NULL); ASSERT(length \u0026lt;= MAX_LENGTH); if (length == inode-\u0026gt;length) { return true; } ASSERT(length \u0026gt; inode-\u0026gt;length); size_t origin = bytes_to_sectors(inode-\u0026gt;length); size_t need = bytes_to_sectors(length); // 不需要分配新块的情况 if (origin == need) { inode-\u0026gt;length = length; return true; } lock_acquire(\u0026amp;free_map_lock); // 是否剩余空间够用 if (free_map_available_space() \u0026lt; need - origin) { lock_release(\u0026amp;free_map_lock); return false; } // 初始化一层引用 if (origin \u0026lt;= NUM_DIRECT \u0026amp;\u0026amp; NUM_DIRECT \u0026lt; need) { free_map_allocate(1, \u0026amp;inode-\u0026gt;indirect); block_write_zero(inode-\u0026gt;indirect); } // 初始化二层引用 if (origin \u0026lt;= NUM_DIRECT_PLUS_INDIRECT \u0026amp;\u0026amp; NUM_DIRECT_PLUS_INDIRECT \u0026lt; need) { free_map_allocate(1, \u0026amp;inode-\u0026gt;doubly_indirect); block_write_zero(inode-\u0026gt;doubly_indirect); } // 分配所有需要的节点 bool success = inode_map_sectors_allocate(inode, origin, need); lock_release(\u0026amp;free_map_lock); inode-\u0026gt;length = length; return success; } 分配一次引用和二次引用关键是计算需要多少块，然后什么范围需要一次引用或二次引用\n这里就不讲了\n子目录 目录的本身也是一种文件数据块\n前面可扩展大小文件也看到类型声明里面就有相关的代码\n目录本身其实也可以看作是一个文件\n只不过这个文件保存的就是目录名字\n这部分的工作主要在filesys中完成\n有了前面的实现，其实这步主要是一些逻辑部分\n这里只举两个例子，因为别的也差不多，看源代码吧\n下面是文件创建的代码\nbool filesys_create(const char *name, off_t initial_size) { // 打开当前线程的工作路径 struct dir *dir = dir_open_cwd(name); struct inode *inode = NULL; const char **name_cp = \u0026amp;name; while (true) { if (!dir_is_valid(dir)) { return false; } char part[NAME_MAX + 1]; // 获取下一个目录名 int status = get_next_part(part, name_cp); if (status \u0026lt; 1) { return false; } // 创建文件 if (**name_cp == \u0026#39;\\0\u0026#39;) { block_sector_t inode_sector = 0; bool success = (free_map_allocate(1, \u0026amp;inode_sector) \u0026amp;\u0026amp; inode_create_with_dir_info(inode_sector, initial_size, get_dir_inumber(dir), false) \u0026amp;\u0026amp; dir_add(dir, part, inode_sector)); if (!success \u0026amp;\u0026amp; inode_sector != 0) { free_map_release(inode_sector, 1); } dir_close(dir); return success; } // 打开下一级目录 dir_lookup(dir, part, \u0026amp;inode); dir_close(dir); dir = dir_open(inode); } return false; } 这个是切换工作路径的代码\nblock_sector_t filesys_chdir(const char *path_name) { // 空返回 if (path_name == NULL || (\u0026#39;\\0\u0026#39; == path_name[0])) { return 0; } // 如果要返回上一级 if (!strcmp(path_name, \u0026#34;..\u0026#34;)) { return inode_get_parent_dir(inode_open(thread_current()-\u0026gt;cwd)); } const char **name_cp = \u0026amp;path_name; struct dir *dir = dir_open_cwd(path_name); while (true) { struct inode *inode = NULL; char part[NAME_MAX + 1]; // 获取下一级目录 get_next_part(part, name_cp); if (dir != NULL) { dir_lookup(dir, part, \u0026amp;inode); } dir_close(dir); // 如果已经找到最后 if (**name_cp == \u0026#39;\\0\u0026#39;) { if (inode == NULL) { return 0; } return inode_get_inumber(inode); } if (inode == NULL) { return 0; } dir = dir_open(inode); if (dir == NULL) { return 0; } } return 0; } 系统调用chdir是调用这个代码\n剩下的部分也都是这样，用已经实现的结构写逻辑就可以了\n然后记得写系统调用那边的实现就可以了\n\u0026lt;0x03\u0026gt; 测试相关 如果跟我一样没写相关的PATH，那么就要改下make.var\n","date":"2025-01-06T05:05:48+08:00","permalink":"/post/categories/cs162/zjut-os-05-%E8%AE%B0%E5%BD%95/","title":"Zjut-Os-05 Filesys 记录"},{"content":"\u0026lt;0x01\u0026gt; 任务说明 这里将实现pintos的用户程序的执行\n参数传递 process_execute函数可以在pintos中创建新的用户进程\n当前，这个函数还不支持参数的传递\n比必须实现参数传递，让用户程序的main中argc和argv可以访问参数\n举例来说，如果执行process_execute(\u0026quot;ls -ahl\u0026quot;)\n在用户程序的argc的值是2，argv的值是[\u0026quot;ls\u0026quot;, \u0026quot;-ahl\u0026quot;]\n因为大部分的测试会先打印自己的测试名\n这会访问argv[0]\n但如果没有实现这个部分，这些测试就会在访问argv[0]时崩溃\n所以在实现这个部分前，大部分测试都会直接崩溃\n进程控制系统调用 目前，pintos只支持exit这个系统调用\n这个调用会终止发起调用的进程\n还需要实现practice、halt、exec、wait这几个系统调用\n实现指南 要实现这写系统调用，首先需要安全地读写用户虚拟地址空间地内存\n系统调用的参数都在用户进程栈中\n在处理非法指针与空指针时不能让内核崩溃\n另外的，一些系统调用的参数由指针指向另一块内存区域\n这个内存区域也可能会是非法的\n尽量避免为实现系统调用编写大量重复的代码\n每个系统调用参数(无论是整数还是指针)在堆栈上占用 4 个字节\n应该能够利用这一点，避免编写大量几乎相同的代码 以便从堆栈中检索每个系统调用的参数。\n需要优雅地处理无效内存访问而无法完成系统调用的情况\n比如空指针、无效指针(例如指向未映射内存)和非法指针(例如指向内核内存)\n注意：如果内存位于页边界上\n那么4字节内存区域(例如32位整数)可能由2字节的有效内存和2字节的无效内存组成\n这时候应当终止用户进程\n我们建议在实现任何其他系统调用功能之前测试代码的这一部分\n这部分的系统调用 practice int practice (int i) 这个系统调用是虚构的，主要是用来熟悉系统调用处理方法的\n作用是传入一个数，返回这个数+1\nhalt void halt (void) 通过调用device/shutdown.h中的shutdown_power_off函数来终止系统\n实际上很少使用\nexit void exit (int status) 终止当前的用户程序，然后将状态返回给内核\n当用户程序退出时，还需要打印用户程序的退出状态\n格式为(进程名): exit(退出状态)\nexec pid_t exec (const char *cmd_line) 运行cmd_line的可执行文件，并返回新进程的pid\n如果没法执行就返回-1\n由于父进程实际上是不知道子进程是否执行完毕，所以需要使用合适的同步方法\nwait int wait (pid_t pid) 等待子进程的pid并检索子进程的退出状态\n如果pid仍然存在，那么就等待其终止，返回退出的状态\n如果pid没有exit就终止了，那么就返回-1\n对于A的子进程B的子进程C，A等待C是不合法的，返回值-1\n如果某个进程已经被等待了，那么返回的也应当是-1\n进程可以产生任意数量的子进程，以任意顺序等待\n甚至可能在没有等待子进程的情况下退出\n必须考虑所有的可能，确保资源正确的释放\n这部分的工作主要在userprog/process.c中的process_wait中完成\n实现wait需要更多的工作\n文件操作系统调用 除了上面的进程控制系统调用，还需要实现下面的文件操作系统调用\ncreate、remove、open、filesize、read、write、seek、tell、close\n在pintos中已经包含了一个基本的文件系统，已经实现了这些功能\n这意味着只需要添加系统调用部分的代码即可\n需要注意的是，大部分测试靠write系统调用输出内容\n如果没有实现write，大部分测试将无法输出内容\n实现指南 pintos的文件系统不是线程安全的\n因此必须确保文件操作系统调用不会同时调用多个文件系统函数\n在filesys中，将为pintos文件系统添加更复杂的同步\n目前，可以对文件操作系统调用使用全局锁\n我们建议您避免修改此项目中的filesys/目录\n当用户进程运行时，必须确保没有人可以修改磁盘上的可执行文件\nRox-*测试检查这是否正确实现\nFile_deny_write和file_allow_write函数可以帮助实现这个特性\n拒绝写入支持实时进程的可执行文件非常重要\n因为操作系统可能会从文件中延迟加载代码页\n或者可能会将一些代码页翻页出来，然后再从文件中重新加载\n在pintos中，目前这不是一个问题，因为文件在执行开始之前被完整地加载到内存中\n而且pintos不实现任何类型的请求分页\n但是，仍然需要实现这一点，因为这是一个很好的实践\n项目用户程序的最终代码将用作项目文件系统的起点\n项目文件系统的测试依赖于您为该项目实现的一些相同的系统调用\n您可能必须修改其中一些系统调用的实现，以支持项目文件系统所需的其他特性\n虽然您肯定不能提前很长时间为项目文件系统做计划\n但是我们建议您编写好的代码，通过保存好的文档可以很容易地修改这些代码\n这部分的系统调用 create remove open filesize read write seek tell close 浮点操作 pintos目前不支持浮点操作\n你需要实现这个功能，让系统和用户程序可以使用浮点的指令\n实现指南 这可能看起来是一项艰巨的任务\n但其实，当涉及到实现浮点指令时，操作系统不需要做太多\n编译器负责生成浮点指令，硬件负责执行浮点指令\n但是，由于CPU上只有一个浮点运算器，这意味着所有线程都必须共享它\n因此，操作系统必须在上下文切换、中断和系统调用期间在堆栈上保存和恢复浮点寄存器\n就像它对通用寄存器所做的那样\n然而，不同的是，当创建新线程或进程时，FPU寄存器需要正确初始化 操作系统也需要在启动期间初始化FPU\n本课程的学习目标不是学习浮点操作的细节\n而是了解线程上下文切换的细节\n由于此任务处理一些操作系统初始化和上下文切换代码\n因此这些文件中的错误可能会导致一些严重的、无法破译的错误\n这些错误是由损坏线程导致的\n我们建议您在完成所有其他任务之后再进行浮点操作\n这部分的系统调用 compute_e \u0026lt;0x02\u0026gt; 代码实现 参数传递 这个部分的理论知识是01-Introduction部分的x86调用约定\n这里就不再写了\n这部分有下面两步\n先是把传入的命令行按空格分隔\n然后把分隔出的东西一个一个放入内存中\n按空格分隔 这个相对简单，pintos提供了strtok_r函数\n这个函数会返回分隔后的const char*串\nwhile (token = strtok_r(cli, \u0026#34; \u0026#34;, \u0026amp;cli)) { // token就是分隔好的字符串 } 按调用约定放入内存中 这里跟之前的调用约定有些出入\npintos的stack_align-*系列测试中，会检查参数栈是否对齐\n具体来说就是最后argc的内存地址可以被16整除\n// 将参数压入栈 void process_push_args(const char *cli, void **if_esp) { void *esp = *if_esp; int argc = 0; char *argv[64]; int args_size = 0; unsigned int cli_size = strlen(cli) + 1; char *cli_copy = malloc(cli_size); strlcpy(cli_copy, cli, cli_size); // argv[...][...] char *token = NULL; while (token = strtok_r(cli_copy, \u0026#34; \u0026#34;, \u0026amp;cli_copy)) { int size = strlen(token) + 1; esp -= size; argv[argc] = esp; strlcpy(esp, token, size); args_size += size; argc++; } // stack-align args_size = args_size + sizeof(char *) * (argc + 1) + sizeof(char **) + sizeof(int); int temp = args_size % 0x10; if (temp \u0026gt; 0) { int align_size = 0x10 - temp; esp -= align_size; memset(esp, 0, align_size); } // argv end esp -= 0x04; *(char **)esp = 0; // argv[0]... for (int i = argc - 1; i \u0026gt;= 0; i--) { esp -= sizeof(char *); *(char **)esp = argv[i]; } // argv ptr esp -= 0x04; *(char ***)esp = (esp + 0x04); // argc esp -= 0x04; *(int *)esp = argc; // return address esp -= 0x04; *(int *)esp = 0; *if_esp = esp; } 在start_process的合适位置加上然后传参即可\n查看传入的参数 现在我们能传进去了，但我们该怎么检查呢\n这时候需要有一个调式函数，用来输出当前的参数堆栈\n// debug 打印堆栈中的参数信息 void process_debug_print_argument_stack(void) { void *esp = 0xbfffffc0; const int offset = 0xfc0; const int show_size = 1 \u0026lt;\u0026lt; 6; void *upage = pg_round_down(esp); printf(\u0026#34;(DEBUG): Print Argument Stack\\n\u0026#34;); hex_dump((uintptr_t)upage + offset, upage + offset, show_size, true); printf(\u0026#34;(DEBUG): end\\n\u0026#34;); } 传参完调用这个函数就可以打印了，就是提交的时候记得注释\n系统调用 这里我把所有系统调用合在一起讲了\n用户程序怎么调用系统调用 关于系统调用主要有下面这几个文件\n/lib/user/syscall.* /lib/syscall-nr.h /userprog/syscall.* /lib/user/syscall.*是用户空间下系统调用的定义 /lib/syscall-nr.h枚举了所有的系统调用类型 /userprog/syscall.*是内核系统调用的实现\n怎么理解呢，首先先看/userprog/syscall.c里面的syscall_init\n里面有一句intr_register_int(0x30, 3, INTR_ON, syscall_handler, \u0026quot;syscall\u0026quot;);\n表示注册中断向量0x30的系统调用中断\n这个函数会在/threads/init.c里面调用，所以一启动就会执行\n再来看/lib/user/syscall.c的东西\n会发现里面有很多汇编的东西，其实问题不大，这些不关键\n这些汇编做的事情就是把参数放在内存中，然后触发系统调用中断\n当用户程序调用系统调用时\n会触发系统调用中断，这时候会跳转到syscall_handler函数中\n然后在里面做一些处理\n系统调用内存布局与寄存器 根据/lib/user/syscall.c里面的汇编\n不难发现参数内存布局是这样的\n低地址-\u0026gt;高地址\rtype describe\rint syscall_type \u0026lt;-esp\r* arg0\r* arg1\r* arg2 这里举例3个参数的情况，实际上3参数的系统调用就3个\n其他情况就少几个传入的参数而已\n传入的参数类型是4字节的数或指针\n在触发系统调用中断后，我们就可以通过esp指针访问这些参数\n如果系统中断有返回值，那么还需要将返回值放到eax中\n检查参数是否合法 前面也提到，用户程序使用系统调用时，可能传递非法参数\n比如说传递空指针或者尝试访问内核的内存空间\n这些是不被允许的，但我们也不能让内核直接崩溃\n所以就需要检查参数是否合法\n对于不合法的情况，就需要关闭这个用户进程\n我们就需要写一个函数来检查指针\nbool is_validity(uint32_t *return_ptr, const void *uaddr) { // 检查是否是用户的内存空间，检查是否为空指针 if (!is_user_vaddr(uaddr) || (pagedir_get_page(process_current()-\u0026gt;pagedir, uaddr) == NULL)) { syscall_exception_exit(return_ptr); return false; } return true; } syscall_exception_exit作用就是退出进程，返回值-1\n统一入口处理系统调用 在触发系统调用中断时，实际上就是执行syscall_handle函数\n这时候需要通过esp指向的第一个数值来判断系统调用的类型\n#define SYSCALL_COUNT 31 static void (*syscall_list[SYSCALL_COUNT])(uint32_t *return_ptr, uint32_t *args_stack); // ... static void syscall_handler(struct intr_frame *f UNUSED) { uint32_t *args_stack = ((uint32_t *)f-\u0026gt;esp); uint32_t *return_ptr = \u0026amp;f-\u0026gt;eax; // 判断syscall_type的地址是否合法 is_validity(return_ptr, args_stack); // 判断第一个参数地址是否合法 is_validity(return_ptr, args_stack + 1); int syscall_type = args_stack[0]; if (syscall_type \u0026lt; 0 || syscall_type \u0026gt; SYSCALL_COUNT) { syscall_exception_exit(return_ptr); } syscall_list[syscall_type](return_ptr, args_stack); } 由于C中的enum本质就是int数值，所以syscall_type是当前的系统调用类型\nsyscall_list是一个函数指针表，根据不同的类型执行不同的代码\n比较简单的实现还是看仓库代码吧，这里就不写了\n进程的等待 原本的等待实现是非常简单的\n全局有个temporary的信号量\n原本实现就是等这个信号量释放\n而这个信号量的释放就在进程退出的时候\n这样的实现是对的\n但如果说要实现wait系统调用等功能的话这就不够用了\n全局线程查找表 要实现wait的功能，首先需要有个结构保存进程间的父子关系等信息\n这里采用全局的list来保存进程间的信息\nstruct thread_node { struct list_elem elem; // 给list用的 tid_t current_tid; // 当前线程tid tid_t parent_tid; // 父线程tid int ret_status; // 线程返回值 struct semaphore exit_sema; // 退出信号量 struct semaphore load_sema; // 加载信号量 bool is_already_wait; // 是否已经等待过 bool is_load_success; // 是否加载成功 }; 进程的退出 进程的文件操作 浮点操作 这个东西乍一看很头疼\n实际上确实头疼，涉及一些汇编的东西\n并且如果改错一些东西会导致内核无法启动，调试都很难\nx86的fpu 不同于通用地址寄存器，x86的fpu没有可独立寻址的浮点寄存器\n但它有一个堆栈寄存器\n要操作fpu，就需要在这个堆栈中推送和弹出\n比方说计算PI的平方根\n那么首先要将PI压入栈，然后使用fsqrt指令\n这个指令会弹出一个栈的内容，然后计算完后再放回栈\n所以计算完后，栈顶内容就是PI的平方根\n在和fpu交互方面，不同于通用寄存器的直接操作的方式\nfpu需要使用108字节的结构表示fpu的状态\n对fpu的交互需要通过这样的结构\nSize\tData | Size Data\r2\tControl Word | 10 ST(0)\r2\t(unused) | 10 ST(1)\r2\tStatus Word | 10 ST(2)\r2\t(unusued) | 10\tST(3)\r2\tTag Word | 10 ST(4)\r2\t(unused) | 10 ST(5)\r4\tInstruction Pointer | 10 ST(6)\r2\tCode Segment | 10\tST(7)\r2\t(unused)\r4\tOperand address\r2\tData Segment\r2\t(unused) 在操作系统启动期间，不需要保存现有的fpu状态集，因为没人要用\n但是在另一个线程/进程初始化时，就必须保存fpu状态避免丢失浮点数据\n实现这个的最简单的办法是将fpu寄存器状态保存到临时位置，然后初始化\n启用fpu 在pintos中，默认不允许使用fpu的，首先要先启用它\n// threads/start.s // line 150 // orl $CR0_PE | CR0_PG | CR0_WP | CR0_EM, %eax orl $CR0_PE | CR0_PG | CR0_WP, %eax 保存fpu的信息 现在启用了fpu，但在中断处理和线程上下文切换时，fpu的信息没有保存\n这里要改很多地方，目的都是添加中断时fpu的信息\n（由于笔者超小杯汇编，实在解释不明白，见谅）\n// threads/interrupt.h #define FPU_SIZE 108 struct intr_frame { // ... uint8_t fp_regs[FPU_SIZE]; // 添加浮点寄存器 // ... } // threads/intr-stubs.s .func intr_entry intr_entry: /* 新加的保存fpu状态 */ subl $FPU_SIZE, %esp fsave (%esp) pushl %ds pushl %es pushl %fs pushl %gs pushal cld mov $SEL_KDSEG, %eax mov %eax, %ds mov %eax, %es leal 56(%esp), %ebp pushl %esp .globl intr_handler call intr_handler addl $4, %esp .endfunc .globl intr_exit .func intr_exit intr_exit: popal popl %gs popl %fs popl %es popl %ds /* 新加的恢复fpu状态 */ frstor (%esp) addl $FPU_SIZE, %esp addl $12, %esp iret .endfunc 还有线程上下文的部分\n// threads/switch.h #define FPU_SIZE 108 // ... struct switch_threads_frame { // ... uint8_t fp_regs[FPU_SIZE]; // fpu状态 // ... }; // ... // 因为加了108字节的fpu状态，所以就+108 #define SWITCH_CUR 128 #define SWITCH_NEXT 132 // threads/switch.s .globl switch_threads .func switch_threads switch_threads: /* 新加的保存fpu状态 */ subl $FPU_SIZE, %esp fsave (%esp) pushl %ebx pushl %ebp pushl %esi pushl %edi .globl thread_stack_ofs mov thread_stack_ofs, %edx movl SWITCH_CUR(%esp), %eax movl %esp, (%eax,%edx,1) movl SWITCH_NEXT(%esp), %ecx movl (%ecx,%edx,1), %esp popl %edi popl %esi popl %ebp popl %ebx /* 恢复fpu状态 */ frstor (%esp) addl $FPU_SIZE, %esp ret .endfunc .globl switch_entry .func switch_entry switch_entry: /* 初始化fpu */ finit addl $8, %esp pushl %eax .globl thread_switch_tail call thread_switch_tail addl $4, %esp ret .endfunc 用户线程中恢复fpu状态 现在已经保存了fpu状态了，但还没导入用户进程中\n// userprog/process.c static void start_process(void *cli_) { // ... // 加在最后，导入fpu状态 asm(\u0026#34;fsave (%0)\u0026#34; : : \u0026#34;g\u0026#34;(\u0026amp;if_.fp_regs)); /* Start the user process by simulating a return from an interrupt, implemented by intr_exit (in threads/intr-stubs.S). Because intr_exit takes all of its arguments on the stack in the form of a `struct intr_frame\u0026#39;, we just point the stack pointer (%esp) to our stack frame and jump to it. */ asm volatile(\u0026#34;movl %0, %%esp; jmp intr_exit\u0026#34; : : \u0026#34;g\u0026#34;(\u0026amp;if_) : \u0026#34;memory\u0026#34;); NOT_REACHED(); } 完善float库 原本提供的lib/float.h的实现是不完善的，需要进一步实现\n// lib/float.h static inline void fpu_push(int num) { unsigned char fpu[108]; // 内联asm获取fpu状态 asm(\u0026#34;fsave %0;\u0026#34; : : \u0026#34;m\u0026#34;(fpu)); // 获取栈顶 int status_word = fpu[4]; // 将数字压入 fpu[28 + status_word * 10] = (float)num; // 确保status_word在0-7中循环 if (status_word == 7) { status_word = 0; } else { ++status_word; } fpu[4] = status_word; // 存入fpu asm(\u0026#34;frstor %0;\u0026#34; : : \u0026#34;m\u0026#34;(fpu)); } // 差不多就不写注释了 static inline int fpu_pop(void) { int val; unsigned char fpu[108]; asm(\u0026#34;fsave %0;\u0026#34; : : \u0026#34;m\u0026#34;(fpu)); int status_word = fpu[4]; if (status_word == 0) { status_word = 7; } else { status_word--; } val = fpu[28 + status_word * 10]; fpu[4] = status_word; asm(\u0026#34;frstor %0;\u0026#34; : : \u0026#34;m\u0026#34;(fpu)); return val; } 实现系统调用 比起前面，这步算简单了\n跟前面实现系统调用的部分一样，为compute_e开一个函数处理即可\n别忘了就行，这里就不讲了\n","date":"2025-01-05T03:55:19+08:00","permalink":"/post/categories/cs162/zjut-os-03-%E8%AE%B0%E5%BD%95/","title":"Zjut-Os-03 Userprog 记录"},{"content":"为什么没06 因为新代码就不要求虚拟内存\n我写完这些剩下的时间也不多了，而且还有别的课设\n所以这部分就摸了\n对pintos整个的体验 说实在的，这个项目挺难的\n很多时候感觉这个项目太自由，导致我几乎无从下手\n但这个项目其实也挺好的\n因为它给了一个不高不低的起点\npintos本身已经实现了基本的启动之类的功能\n实际开发时是不需要动很多汇编的东西的\n但起码要看懂寄存器之类的含义\n目前pintos主要有两个版本\n一个是使用最广泛的版本，PKU、KAIST等都在用的版本\n另一个就是我们用得跟CS162同步的版本\n使用广泛的版本相对简单些，参考多，实现的功能也少一点\n我也建议如果是自学的话还是用这个流传广泛的版本会舒服一些\n等写完这个版本再看看CS162的版本也来得及，大部分代码思路还是通用的\n新版本和老版本比较 新版本不要求vm的部分，然后userprog作为project-1\n跟老版本比的话新版本的内核空间和用户空间判断更严了\n然后每个子项目对比userprog多了fpu\nthreads多了user thread的内容，少了BSD调度\nfilesys之前没写，所以我也不清楚\n新版本的门槛比老版本高许多\n毕竟是userprog起手，上来就要了解下调用约定\n而且introduction部分还有一点啸问题\n不像老版本起手alarm-sleep修改，前置知识少很多\n起码我之前写老版本的时候逻辑是连贯的\n但写新版本的代码很容易卡住，然后到处查东西\n测试机的体验 我是历史学家，这就是史\n会出现相同代码跑测试会跑出不同的分数\n我只能说我没意见好吧\n如果说测试机上面跑两次就会有不同的结果\n那我线下测试这么多次老早就改明白了，所以是谁的问题呢\n然后还有交threads的部分\n老代码确实有THREADS的编译宏，但新的代码基有THREADS、USERPROG、FILESYS这仨\n测试机编译莫名其妙只有THREADS的部分，过不了是我的问题吗\n而且最猛的是，我直接交代码基，一行不改，也是这样，只能说真没人检查过啊\n（我当然知道只要我的代码跟代码基用同样的编译宏就肯定有一样的问题，只是这样测试过之后更觉得学校草台）\nmake[1]: Entering directory \u0026#39;/home/vagrant/ag/pintos/src/threads/build\u0026#39;\rgcc -m32 -c ../../threads/thread.c -o threads/thread.o -ggdb3 -O0 -march=i686 -fno-pic -fno-inline -fno-stack-protector -nostdinc -I../.. -I../../lib -I../../lib/kernel -Wall -W -Wstrict-prototypes -Wmissing-prototypes -Wsystem-headers -DTHREADS -MMD -MF threads/thread.d\r../../threads/thread.c: In function \u0026#39;init_thread\u0026#39;:\r../../threads/thread.c:467:6: error: \u0026#39;struct thread\u0026#39; has no member named \u0026#39;pcb\u0026#39;\rt-\u0026gt;pcb = NULL;\r^~\r../../Make.config:53: recipe for target \u0026#39;threads/thread.o\u0026#39; failed\rmake[1]: *** [threads/thread.o] Error 1\rmake[1]: Leaving directory \u0026#39;/home/vagrant/ag/pintos/src/threads/build\u0026#39;\r../Makefile.kernel:10: recipe for target \u0026#39;check\u0026#39; failed\rmake: *** [check] Error 2 理论上这个的编译指令应该是这样的\ngcc -m32 -c ../../threads/thread.c -o threads/thread.o -ggdb3 -O0 -march=i686 -fno-pic -fno-inline -fno-stack-protector -nostdinc -I../.. -I../../lib -I../../lib/kernel -Wall -W -Wstrict-prototypes -Wmissing-prototypes -Wsystem-headers -DTHREADS -DUSERPROG -DFILESYS -MMD -MF threads/thread.d 可以看到少了什么，少了那几个编译宏\n虽然经过我一晚上的测试，发现测试机只测试threads的部分，不会测试userprog和filesys部分\n但本地测试的时候是会全部测试的，谁知道测试机只测threads\n那测试机只有THREAD编译宏确实是能跑的\n而且使用初始代码基也没法跑，所以是谁的问题呢\n而且如果跑filesys测试，会出现下面更搞笑的东西\n以下文件必须包含在您的代码中, 但无法找到. 请确认以下文件都存在 :\r./pintos/src/inode.c filesys的源文件inode.c直接放跟目录是吧\n原始代码基也会这样\n（不过把这个文件复制一份到根目录也就行了，只不过意义不明）\n或许就不应该有这样的期待，我还以为学校总算有点好的课程了，但期待越高，砸得越狠\n但经过这学期别的抽象课程洗礼后，只能说这门课好歹尝试引入国外优秀课程，但结果坏了\n总比某门vc6.0实践技术要好一点点\n我为什么完整记录我的开发过程 其实很简单，因为我发现中文互联网上貌似没有高质量的pintos的开发记录\n以至于我前期遇到一些问题的时候只能啃外网的资料\n（也不是不能啃，但不是原生语言，效率就比较低）\n学校给的文档是机翻的，还不如啃原文\n（生草机强力驱动）\n而且后来发现之前写的和学校要求的都不是一个版本\n（操作系统老师开学就开始提，也没说这个事情）\n加上新的代码，又缺少相关的文档，连起码的任务说明都没有，写得是真的很难受\n项目是好项目，但学校执行的是真的很烂\npintos在正主学校里面，是一个持续一个学期的项目\n除了pintos的源码与文档，还配有完整的视频课程和助教阵容\n而我们就没那么好运了，学校给的只有文档，这我认为是不够的\n考虑到我是数媒专业的学生，汇编语言这门课不在我的专业课中\n所以在我写到系统调用的时候，我基本上是再学一门汇编语言这样写下去的\n好在油管上我能找到KAIST大学的pintos的视频课，很多概念看着视频也就理解了\n于是我觉得我有必要完整记录我的开发过程\n还有一件小事\n我们学校操作系统课程设计是有一个QQ群的\n有一次我看到有个同学发一个错误\n里面写的是大概意思就是找不到clang，问怎么解决\n我写这段不是想表示怎么这个都不懂\n如果我是老师，我都不会怪这位同学\n因为学校确实是没讲过C/C++有哪些编译器，编译是怎么样的流程\n甚至这个课设也没讲该怎么讲环境配置\n（准确来说讲的是老版本的环境配置）\n你说这些东西基础吗，那确实是基础\n但现状就是大部分人可能连自己写的程序用什么编译器都不知道\n我认为，如果学校觉得这个是常识但有人还不知道，那就是学校没教好\n因为起码目前看，大学是不会按学生的专业能力分专业的\n一路考试过来，能确定的是学生的语数英和选课成绩但不能确定学生的计算机水平\n如果想从这样的生源中培养计算机学生，学校必须把完整的一整套知识全部梳理一遍\n这样可以保证对计算机不熟的学生，在大学4年后能有足够的能力应对工作\n不然就像现在，我觉得我们基本就是自学，老师的作用就是布置课设\n通过课设筛选有能力的学生，剩下相对没能力的学生就好像被抛弃了一样\n关于C和Rust 这个算题外话\n我写这个项目前，我习惯用的都是“GC系”的语言\n（就是使用垃圾回收机制的语言）\n虽然C++会一点，但一直不敢入门C，感觉会非常烧脑\n全是指针，完全面向过程，没有一点对象，我感觉把握不了\n学校操作系统课设要写这玩意，那不得不上马了\n真上手写C的时候，刚开始确实很痛苦\n怎么没函数重载，怎么没class，指针还满天飞\n然后写了几段代码，很快就上手了\n我现在感觉，C其实C++简单\nC里面很纯粹，就是值传递和指针传递\n而且万物都可以是指针，只要能穿明白类型，非常自由\n就像真的手操内存一样，写起来也像是汇编一样自由\n而且没有重载之类的，链接器也不容易出问题\n反观C++，在入门的时候，连接器是天天报错的，而且很容易一头雾水\n然后有赋值传递，指针传递，引用传递，很容易乱\n其他还有很多比较劝退的点\n当然也可以说我学的不好，但不得不承认C在概念上确实是比C++简单得多\n在写这段项目前，我室友学Rust的时候，我跟着看了一段时间\n他是站Rust拥护者那边，我那时候是站对立面的\n我每天都会在那边扣Rust语法怎么神秘，缺少什么语法糖\n他就说这样设计挺好的，没啥问题，能用\n在我写这个项目之后，我现在对Rust改观还是挺大的\n我不应该把Rust看作是Java、C#的代替品\n如果把Rust看着是C和C++的代替品，那确实能解决很多问题\n可以既有足够底层的交互又有强类型的限制\n我觉得对于一门编程语言，首先要考虑的是它面对的问题\n对于Python、C#之类的语言，它们面对的更多是业务代码\n那就需要足够多的语法糖来提高代码的可读性\n而对于C这样的语言，它需要面对的更多是硬件和系统的交互\n对它来说更关键的是足够的底层，要尽量少的抽象\n如果写业务逻辑，虽然Rust也能写，但我不会选择Rust\n但如果跟硬件和系统打交道，我之后还真可能会选择Rust试试\n","date":"2024-12-28T01:00:28+08:00","permalink":"/post/categories/cs162/zjut-os-%E5%90%8E%E8%AE%B0/","title":"Zjut-OS 后记"},{"content":"这个是CS162的Project 0，也是我们的实验一部分\n虽然这个部分不必出现在最后的验收和报告中\n但里面的坑还是很多的，我觉得还是要讲一讲\n\u0026lt;0x01\u0026gt; 任务说明 这个任务很简单，在pintos的源码中有一些小问题\n这会使pintos在启动用户进程时崩溃\n你需要找出这个错误并修改\n这个任务最简单的解决办法只需要更改一处代码\n这个任务需要通过的测试是do-nothing和stack-align-0\n\u0026lt;0x02\u0026gt; 省流版 只需要在./userprog/process.c中作这样的修改\nstatic void start_process(void *file_name_) { // ... /* Initialize interrupt frame and load executable. */ if (success) { memset(\u0026amp;if_, 0, sizeof if_); if_.gs = if_.fs = if_.es = if_.ds = if_.ss = SEL_UDSEG; if_.cs = SEL_UCSEG; if_.eflags = FLAG_IF | FLAG_MBS; success = load(file_name, \u0026amp;if_.eip, \u0026amp;if_.esp); } // 新加的一句 if_.esp -= 0x14; /* Handle failure with succesful PCB malloc. Must free the PCB */ if (!success \u0026amp;\u0026amp; pcb_success) { // Avoid race where PCB is freed before t-\u0026gt;pcb is set to NULL // If this happens, then an unfortuantely timed timer interrupt // can try to activate the pagedir, but it is now freed memory struct process *pcb_to_free = t-\u0026gt;pcb; t-\u0026gt;pcb = NULL; free(pcb_to_free); } // ... } 只要在这个位置加上if_.esp -= 0x14;就可以了\n\u0026lt;0x03\u0026gt; 原理 需要指出的是，笔者目前没系统学过汇编，对x86调用约定之类的只知道皮毛\n下面的内容有部分是我猜测出来的，有概率是错的，如果有误请务必指出\n问题原因 是不是感觉很奇怪，为什么加上这句就可以解决加载用户进程的出错\n实际上，让我们先删除这句，看看原本的pintos启动do-nothing的时候会报什么错\nExecuting \u0026#39;do-nothing\u0026#39;:\rPage fault at 0xc0000008: rights violation error reading page in user context.\rdo-nothing: dying due to interrupt 0x0e (#PF Page-Fault Exception).\rInterrupt 0x0e (#PF Page-Fault Exception) at eip=0x8048915\rcr2=c0000008 error=00000005\reax=00000000 ebx=00000000 ecx=00000000 edx=00000000\resi=00000000 edi=00000000 esp=bfffffe4 ebp=bffffffc\rcs=001b ds=0023 es=0023 ss=0023\rExecution of \u0026#39;do-nothing\u0026#39; complete. 我们可以看到，这里报了个Page fault\n根据pintos的文档，说明这里是用户进程尝试访问内核空间导致的错误\nA user program can only access its own user virtual memory. An attempt to access kernel virtual memory causes a page fault, handled by page_fault() in userprog/exception.c, and the process will be terminated. 一个用户程序只能访问自己的用户虚拟内存\r如果尝试访问内核虚拟内存会导致页面错误\r然后会在 ./userprog/exception.c 的 page_fault() 中处理，用户进程会被终止 我们可以在start_process()打上断点，看看最后的esp是什么\n然后我们可以得到这样的数值\nesp: 0xc0000000 根据pintos的文档，这个数字也是PHYS_BASE的值\nVirtual memory in Pintos is divided into two regions: user virtual memory and kernel virtual memory. User virtual memory ranges from virtual address 0 up to PHYS_BASE, which is defined in threads/vaddr.h and defaults to 0xc0000000 (3 GB). Kernel virtual memory occupies the rest of the virtual address space, from PHYS_BASE up to 4 GB.\rpintos的虚拟内存有两块组成\r用户虚拟内存和内核虚拟内存\r用户虚拟内存地址范围是0 - PHYS_BASE\rPHYS_BASE在 ./threads/vaddr.h 定义并且默认为 0xc0000000 (3 GB)\r内核虚拟内存占据剩下的部分\r也就是PHYS_BASE - 4GB 那现在也就不难理解这个报错了\n报错信息也指出，当前用户进程尝试访问地址为0xc0000008的内容\n为什么会访问这个地址 那么用户进程为什么会在执行前访问这个地址的内容呢\n这就要讲到x86的调用约定与用户程序启动过程\nx86调用约定 对于我们习惯高级语言的程序员来说，处理参数问题是很简单的\nvoid temp(int a, int b); 这样就说明这这个函数需要两个参数，都是int\n如果需要使用直接temp(1, 2)就完成了参数传递\n但对于CPU来说，它该怎么理解这两个参数的传递呢\n换句话说，对于远古时期没有高级语言的开发者，该怎么解决参数传递的问题呢\n这里就是调用约定上场的地方了\n调用约定规定了参数在内存中是如何摆放的，寄存器该怎么设置的\n在pintos中，使用的x86调用约定应该是cdecl\n这个调用约定要求传递的参数从右至左压入内存，并将esp设置在最前面\n0xbffffe70 return address \u0026lt;-- esp\r0xbffffe74 1\r0xbffffe78 2 对于刚刚的例子，内存布局大概就是这样\n这里内存的地址是随便编的，表达个意思\n用户进程启动过程 在pintos中，内核会这样启动用户进程\nvoid _start (int argc, char *argv[]) { exit (main (argc, argv)); } 对于内核，它必须把参数在内存合适的地方安排好，然后设置好寄存器状态\n参数的内存布局和寄存器状态就是上面提到的调用约定\npintos的文档中给了一个例子\n假设内核需要执行/bin/ls -l foo bar\n首先内核会将命令行按空格分开，分成一个一个的\n这里先展示最后的内存布局\nAddress Name Data Type\r0xbfffffcc return address 0 void (*) () \u0026lt;-- esp\r0xbfffffd0 argc 4 int\r0xbfffffd4 argv 0xbfffffd8 char **\r0xbfffffd8 argv[0] 0xbfffffed char *\r0xbfffffdc argv[1] 0xbffffff5 char *\r0xbfffffe0 argv[2] 0xbffffff8 char *\r0xbfffffe4 argv[3] 0xbffffffc char *\r0xbfffffe8 argv[4] 0 char *\r0xbfffffec stack-align 0 uint8_t\r0xbfffffed argv[0][...] /bin/ls\\0 char[8]\r0xbffffff5 argv[1][...] -l\\0 char[3]\r0xbffffff8 argv[2][...] foo\\0 char[4]\r0xbffffffc argv[3][...] bar\\0 char[4] 最下面的部分是实际存放字符串的地方\n然后会有个stack-align，这是为了确保四字节对齐来提高性能\n再上面是argv参数的部分，需要注意的是argv最后有一个留空的部分\n然后是argv本身的地址，指向argv[0]\n之后是argc，表示参数个数\n最后是返回地址，当然对于用户进程来说main是没返回值的，直接给0即可\nesp指向的也是这个返回地址\n访问内核地址的原因 我们假设一个最简单的情况，一个参数也不传递\n那么内存布局应该是这样的\nAddress Name Data Type\r0xbffffff0 return address 0 void (*) () \u0026lt;-- esp\r0xbffffff4 argc 0 int\r0xbffffff8 argv 0xbffffffc char**\r0xbffffffc argv[0] 0 char* 但我们之前得到的esp指向的是0xc0000000\n那么在CPU看来，这个函数的参数布局是这样的\nAddress Name Data Type\r0xc0000000 return address * void (*) () \u0026lt;-- esp\r0xc0000004 argc * int\r0xc0000008 argv * char**\r0xc000000c argv[0] * char* 0xc0000000下面是内核空间的虚拟内存，所以就报错了\n因为这一步我们还没实现参数传递的逻辑\n所以这一步最简单的办法就是在某个位置把esp指针往前移几个字节\n为什么偏移量是0x14 这是为了stack-align-0考虑的\n#include \u0026#34;tests/lib.h\u0026#34; int main(int argc UNUSED, char *argv[] UNUSED) { register unsigned int esp asm(\u0026#34;esp\u0026#34;); return esp % 16; } 可以看到，这个测试会对esp取16的余\n查看测试文件，也可以知道期望的返回值是12\n我们假设我们已经实现了参数传递的逻辑\n那么这个测试的内存布局应该是这样的\nAddress Name Data Type\r0xbfffffdc return address 0 void (*) () \u0026lt;-- esp\r0xbfffffe0 argc 1 int\r0xbfffffe4 argv 0xbfffffe8 char **\r0xbfffffe8 argv[0] 0xbffffff2 char *\r0xbfffffec argv[1] 0 char *\r0xbffffff0 stack-align 0 uint16_t\r0xbffffff2 argv[0][...] stack-align-0\\0 char[14] 这esp % 16一算，就是12\n当然，这里逻辑还没实现，那么就取偏移量20也就是0x14\n这样这两个测试也是满足的\n说到底还是缓兵之计，等到之后实现参数传递的时候就不需要这样写了\n","date":"2024-12-11T16:07:53+08:00","permalink":"/post/categories/cs162/zjut-os-01-%E8%AE%B0%E5%BD%95/","title":"Zjut-Os-01 Introduction 记录"},{"content":"这个对应学校课设是实验二，实现一个shell这个部分\n需要注意的是，pintos这个项目是不包括这个实验的\n这个实验是CS162课程的一个作业，不过我们学校是作为课设的一部分的\n项目代码在这里\n学校用的代码基疑似用的是23年的，这里参考的文档是24年的文档\n（之前的文档只有Berkeley本校的才能看了）\n\u0026lt;0x01\u0026gt; 任务说明 这个实验主要是去构建一个shell，这个shell可以让用户运行与管理程序\n通过构建自己的shell，你将会对这些接口更加熟悉并且更了解其他的shell\n（下面是我自己翻译的，用词会比较随便）\n添加cd和pwd的支持 每个shell支持一系列的内置指令，实际上就是shell内部的函数\n比方说，exit这个指令就是shell自己实现的\n目前只有两个内置指令，help和exit\n添加新的内置指令pwd，可以打印出当前的工作目录\n再添加新的内置指令cd，有一个目标目录的参数，让工作目录切换到目标目录\n执行程序 目前如果你尝试输入什么不是内置指令的命令运行\nshell会返回说它不知道怎么去执行外部程序\n你需要修改你的shell让它可以执行外部程序\n执行的第一个词是程序名，后面跟着的是程序执行的参数\n对于这一步，你可以假设第一个词是程序的完整路径\n比如假设我们要运行wc，实际应该运行的是/usr/bin/wc\n在下一步才需要实现像wc这样的简单的文件名\n如果只实现这步，在自动测试平台中也是可以过一些测试的\n你需要使用定义在tokenizer.c里面的函数来把输入的命令按空格分开\n不需要支持任何转换的特性\n一旦实现这步，你可以按下面的格式执行程序了\n/usr/bin/wc shell.c 当你的shell需要执行程序时，应该fork一个子进程\n父进程应当等待子进程直到完成，然后继续监听其他的命令\n在PATH中寻找程序 如果每次执行程序都要输入完整路径，那太痛苦了\n我们可以定义一系列的环境变量，这是一个string为键与值的哈希表\n其中一个环境变量就是PATH\n里面有一系列按:分割的路径\n当shell执行一个程序，比如说wc，shell会在PATH的每个路径中寻找有没有wc\n修改你的shell，让它可以使用PATH环境变量\n当然，输入完整路径执行的功能也应当要支持\n不要使用execvp()函数，如果使用的话，测试平台将不评分\n（这个函数本身就差不多实现了这步的功能）\n使用execv()函数实现这个功能\n输入输出重定向 当运行一个程序时，从什么文件中输入或者将输出导入什么文件都是非常有用的\n语法[进程] \u0026gt; [文件]告诉shell将某进程的标准输出重定向到一个文件中\n类似的[进程] \u0026lt; [文件]告诉shell用某文件作为某进程的标准输入\n修改你的shell，让它支持这样的功能\n你不需要为内置指令提供这个功能的支持\n也不需要支持标准错误输出的重定向\n你可以假设输入时\u0026lt;和\u0026gt;两边都是有空格的\n需要注意的时，\u0026gt; [文件]不是要传给程序的参数\n管道处理 这一步的任务类似输入输出重定向\n但需要做的是让许多程序的输入输出可以串起来\n实现这样的语法\n[process A] | [process B] | [process C] A进程的输出会成为进程B的输入，进程B的输出会成为进程C的输入\n为了实现这部分的功能，你需要使用pipe这个系统调用\n从历史来看，这部分的内容对于大部分的学生来说是最难的部分\n建议搞明白pipe是什么，怎么用，再动手实现\n（文档里面介绍pipe的部分摸了，找其他文档也差不多）\n信号处理与终端控制 绝大部分的shell可以让你用快捷键来终止或暂停进程\n比如说Ctrl-C和Ctrl-Z\n这会给shell的子进程发送一个信号\n前者给的是SIGINT信号，也即终止当前的程序\n后者给的是SIGTSTP信号，表示让当前程序切换到后台\n如果直接使用这些快捷键，这种信号也会发送到shell本身\n导致shell本身也被终止了\n这不是我们所期待的，我们希望是终止后只终止子进程\n任务是确保每个程序都在自己的进程组中\n当你启动一个进程，这个进程组应当放在前台\n停止信号应该只影响前台程序，而不是后台的shell\n在实现前需要理解下面的操作系统概念\n进程组 每个进程有一个唯一的ID，也就是PID\n每个进程也有一个进程组号，也就是PGID\n默认情况下，PGID跟父进程是一致的\n进程可以通过getpgid、setgpid、getpgrp、setpgrp控制PGID\n需要注意的是，当你启动新的程序时，这个程序可能需求多个进程\n这些进程应当继承同样的PGID\n将每个shell的子进程作为单独的进程组是一个不错的想法\n每个shell子进程的PGID应当与PID相同\n前台终端 每个终端都关联着一个前台进程组ID\n当你按下Ctrl-C的时候，终端会给前台进程组ID的进程发终止信号\n你可以改变哪个进程组在前台，通过tcsetpgrp(int fd, pid_t pgrp)\nfd应该是0，以代表标准输入\n信号一览 信号是各进程间的同步信息\n下面展示一些常见的信号\nSIGINT 默认情况下，应该终止这个程序 SIGQUIT 这个默认也是终止程序，但程序更重视这个终止信号 这个信号也用于尝试导出内存快照时 SIGKILL 强制终止程序，并且程序不能重写这个信号的行为 SIGTERM 类似SIGTERM SIGTSTP 默认暂停程序 SIGCONT 恢复程序 SIGTTIN 让后台程序可以获取键盘输入 实际上是让程序暂时恢复前台状态，获取完就又回到后台暂停 SIGTTOU 让后台程序可以在终端输出 默认情况下行为跟SIGTTIN一致 在你的shell中，你可以使用kill -XXX PID给进程发信号\n这里的-XXX是指信号的代号，比如kill -TERM PID会发送SIGTERM信号\n在C中，你可以使用signal指定如何处理信号\nshell应该忽略大部分信号，而子进程应该响应这些信号\n需要注意的是，fork出的子进程会继承父进程的信号处理方式\n可选：后台处理 目前，shell在执行下一句之前会等待前一个程序执行完毕\n很多shell允许运行指令到后台，只要在命令行的最后加上\u0026amp;\n当后台程序启动后，shell允许启动其他进程而不用等待后台进程完成\n修改shell，让它可以完成上面的事情\n你只需要支持\u0026amp;语法即可，而不是作为内置指令\n实现这个特性的同时，还要加上新的内置指令wait\n这个会等待所有后台任务处理完毕，然后才会返回\n可以假设\u0026amp;会有空格包裹，并且只出现在指令最后\n可选：前后台切换 绝大部分的shell允许运行的进程在前后台切换\n这需要加上两个新的内置指令\nfg [PID]将进程移到前台，这个进程如果被暂停，那么应该恢复\n如果PID没有指定，那么就将最近启动的进程移到前台\nbg [PID]恢复一个后台的暂停状态的进程\n如果PID没有指定，那么应该恢复最近启动的进程\n\u0026lt;0x02\u0026gt; 代码实现 这部分主要讲解思路，代码不会展示得很详细\n前置：将代码分离 我的习惯是把一些相关的代码分离开来，这样会好找一些\n// shell_cmd.h #pragma once #include \u0026#34;tokenizer.h\u0026#34; typedef int cmd_fun_t(struct tokens *tokens); typedef struct fun_desc { cmd_fun_t *fun; char *cmd; char *doc; } fun_desc_t; extern fun_desc_t cmd_table[]; int cmd_get_size(); int cmd_exit(struct tokens *tokens); int cmd_help(struct tokens *tokens); // shell_cmp.c #include \u0026#34;shell_cmd.h\u0026#34; #include \u0026lt;limits.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define unused __attribute__((unused)) #define ARRAY_SIZE(ARR) (sizeof(ARR) / sizeof((ARR)[0])) fun_desc_t cmd_table[] = { {cmd_help, \u0026#34;?\u0026#34;, \u0026#34;show this help menu\u0026#34;}, {cmd_pwd, \u0026#34;pwd\u0026#34;, \u0026#34;show current working path\u0026#34;}, {cmd_cd, \u0026#34;cd\u0026#34;, \u0026#34;change current working path\u0026#34;}, {cmd_exit, \u0026#34;exit\u0026#34;, \u0026#34;exit the command shell\u0026#34;}, }; int cmd_get_size() { return ARRAY_SIZE(cmd_table); } int cmd_help(struct tokens *tokens) { for (unsigned int i = 0; i \u0026lt; ARRAY_SIZE(cmd_table); i++) { printf(\u0026#34;%s - %s\\n\u0026#34;, cmd_table[i].cmd, cmd_table[i].doc); } return 1; } int cmd_exit(struct tokens *tokens) { exit(0); } 然后在shell.c中做一点小修改，然后在makefile中加入shell_cmp.c即可\n后面其他的东西我也会这样单独分离开来，具体操作就不详细写出了\n有一点需要注意，最后提交的代码还是要合起来的，文件结构要跟原来的一样\n我也不知道为啥，反正如果不合并直接交的话会有奇怪问题\n添加cd和pwd的支持 //shell_cmd.h // ... int cmd_pwd(struct tokens *tokens); int cmd_cd(struct tokens *tokens); // ... //shell_cmd.c // ... // 记得添加到内置命令表中 fun_desc_t cmd_table[] = { {cmd_help, \u0026#34;?\u0026#34;, \u0026#34;show this help menu\u0026#34;}, {cmd_pwd, \u0026#34;pwd\u0026#34;, \u0026#34;show current working path\u0026#34;}, {cmd_cd, \u0026#34;cd\u0026#34;, \u0026#34;change current working path\u0026#34;}, {cmd_exit, \u0026#34;exit\u0026#34;, \u0026#34;exit the command shell\u0026#34;}, }; // ... int cmd_pwd(struct tokens *tokens) { char cwd[PATH_MAX]; // 通过系统调用getcwd来获取当前目录 if (getcwd(cwd, sizeof(cwd)) != NULL) { printf(\u0026#34;%s\\n\u0026#34;, cwd); } else { fprintf(stderr, \u0026#34;show work path failed\u0026#34;); return 1; } return 0; } int cmd_cd(struct tokens *tokens) { // 通过系统调用chdir来改变工作目录 if (chdir(tokens_get_token(tokens, 1)) == 0) { return 0; } else { fprintf(stderr, \u0026#34;change work path failed\u0026#34;); return -1; } } // ... 这样就可以了\n执行程序 既然PATH部分要通过execv来实现\n那这步就直接用execv实现得了\nexecv是一个系统调用，写全是这样得execv(const char* path, char* const* argv)\nconst char*是常量字符串\nchar* const*是常量数组，数组内为char*，也即字符串\n（C里面没有string这个东西，只有char*模拟的字符串）\n假设我们要执行一个程序，路径为/usr/bin/example，要传入的参数有111 222 333\n如果在命令行里面，这个很简单，这样运行就可以\n/usr/bin/example 111 222 333 如果转换为execv系统调用，就是这样的\nconst char* path = \u0026#34;/usr/bin/example\u0026#34; char argv[5][4096]; argv[0] = \u0026#34;/usr/bin/example\u0026#34;; argv[1] = \u0026#34;111\u0026#34;; argv[2] = \u0026#34;222\u0026#34;; argv[3] = \u0026#34;333\u0026#34;; argv[4] = NULL; execv(path, argv); 传入的argv数组最开始是运行的路径，最后要有NULL结尾\n知道这些，那就可以开始实现这部分功能了\n// shell.c // ... while (fgets(line, 4096, stdin)) { struct tokens *tokens = tokenize(line); int fundex = cmd_lookup(tokens_get_token(tokens, 0)); if (fundex \u0026gt;= 0) { cmd_table[fundex].fun(tokens); } else { // 假设最多64个参数，肯定是够大了 char *argv[64]; argv[0] = tokens_get_token(tokens, 0); // 因为第一个参数肯定是自己的路径，已经算一个了 int argv_count = 1; // 循环遍历tokens for (int i = 1; i \u0026lt; tokens_get_length(tokens); i++) { // 把获取到的token通过strdup复制一份，避免重复销毁 argv[i] = strdup(tokens_get_token(tokens, i)); argv_count++; } // 最后一个填NULL argv[argv_count] = NULL; // 传参运行 execv(argv[0], argv); } if (shell_is_interactive) { fprintf(stdout, \u0026#34;%d: \u0026#34;, ++line_num); } tokens_destroy(tokens); } // ... （这里仅讲解原理，在我的实现里，我已经将整个程序运行封装成一个函数了）\n在PATH中寻找程序 有了前面的基础，我们只需要添加PATH中寻找的逻辑就可以了\n这里就直接展示我的实现了\n// shell_program.c // ... // 传入的可以是相对路径也可以是完整路径 char *program_get_full_path(const char *path) { char cwd[PATH_MAX]; getcwd(cwd, sizeof(cwd)); strcat(cwd, \u0026#34;/\u0026#34;); // 判断完整路径 if (access(path, F_OK) == 0) { return strdup(path); } // 判断path else { char *env_path = getenv(\u0026#34;PATH\u0026#34;); char full_path[PATH_MAX]; if (env_path == NULL) { fprintf(stderr, \u0026#34;there is no PATH\\n\u0026#34;); return NULL; } char *path_copy = strdup(env_path); char *token = strtok(path_copy, \u0026#34;:\u0026#34;); while (token != NULL) { full_path[0] = \u0026#39;\\0\u0026#39;; strcat(full_path, token); strcat(full_path, \u0026#34;/\u0026#34;); if (access(strcat(full_path, path), F_OK) == 0) { return strdup(full_path); } token = strtok(NULL, \u0026#34;:\u0026#34;); } } return NULL; } // ... 这里我通过access系统调用判断文件是否可访问来判断有没有这个文件\n如果access能找到，那execv也是认这个路径的\n通过getenv系统调用来获取PATH的内容\n通过strtok将PATH按:分隔\n然后就是构建完整路径，通过access判断文件是否存在\n有这个函数后，在前面输入的路径加上这句就可以了\n// ... // 尝试获取文件的完整路径 argv[0] = program_get_full_path(tokens_get_token(tokens, 0)); // ... 输入输出重定向 这里要提到Unix的一个概念：万物皆文件\n无论是键盘输入还是打印机输出，对于系统内核来说这些都是对文件的读写\n对应的，标准输入和标准输出都有对应的“文件”表示\n每个进程会有一个文件描述表，里面列出了这个进程所有控制着的文件\n一般来说，标准输入是第0号，标准输出是第1号\n所以，输入输出重定向，本质上就是将标准输入输出的“文件”定向到真正的文件上\n通过dup2这个系统调用，我们可以将标准输入输出重定向到需要的文件上\ndup2需要两个fd，一个是文件的fd，一个是目的地的fd\n文件的fd可以用open系统调用获得，目的地fd其实就是标准输入输出\n简单写的话可以直接写0或1\n当然为了增强可读性，可以用unistd.h中的STDIN_FILENO和STDOUT_FILENO表示标准输入输出\n具体实现可以看这个例子\n// 通过解析token知道是否要重定向和重定向的文件路径 // 这里就不写这步的过程了 bool is_input_redirect; bool is_output_redirect; char input_file_path[PATH_MAX]; char output_file_path[PATH_MAX]; if (is_input_redirect) { // 获取文件的fd int fd = open(input_file_path, O_RDONLY); if (fd \u0026lt; 0) { fprintf(stderr, \u0026#34;open input failed\\n\u0026#34;); return EXIT_FAILURE; } // 重定向，本质是复制一份fd作为标准输入 dup2(fd, STDIN_FILENO); // 因为上面复制了一份，所以把这里创建的fd关闭即可 close(fd); } // 下面的原理一样 if (is_output_redirect) { int fd = open(output_file_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd \u0026lt; 0) { fprintf(stderr, \u0026#34;open output failed\\n\u0026#34;); return EXIT_FAILURE; } dup2(fd, STDOUT_FILENO); close(fd); } 管道处理 这个确实比较头疼\n其实可以想象一堆进程通过单向的管道串起来\n这个任务的关键其实是处理一个进程的输入是上一个进程的输出\n也就是这个进程在管道读的这头，上个进程在管道写的这头\n上个进程 \u0026gt;-\u0026gt; 写 \u0026gt;---\u0026gt; 读 \u0026gt;-\u0026gt; 这个进程 \u0026gt;-\u0026gt; 写 \u0026gt;---\u0026gt; 读 \u0026gt;-\u0026gt; 下个进程 为了进程管理方便，这里要使用fork来创建子进程\nfork这个系统调用很有意思，这个函数有两个返回值\n乍一听，挺反常的，实际上这两个返回值一个给父进程，一个给子进程\n给父进程的是子进程的pid，给子进程的是0值\n如果fork失败，返回的是负数\n举个例子\npid_t pid = fork(); if(pid == 0) { printf(\u0026#34;child\\n\u0026#34;); } else { printf(\u0026#34;parent\\n\u0026#34;); } 在程序执行到fork时，会立刻创建一个子进程\n通过下面的if可以写出子进程和父进程不同的处理逻辑\n下面先讲我的实现思路\n// 就是有几个 | 符号 int pipe_count; // 管道们 int pipes[pipe_count][2]; // 初始化管道 for(int i=0; i\u0026lt;pipe_count; i++) { pipe(pipes[i]); } pid_t child_pid; // 表示执行到第几个pipe int pipe_index; // 子进程创建循环 for(...) { child_pid = fork(); if(child_pid == 0) { // ... if (pipe_index \u0026gt; 0) { // 将自己的标准输入替换为上一个的输出 dup2(pipes[pipe_index - 1][0], STDIN_FILENO); } if (pipe_index \u0026lt; pipe_count) { // 将自己的标准输出替换为下一个的输入 dup2(pipes[pipe_index][1], STDOUT_FILENO); } // 然后关闭所有的pipe，确保pipe里面干净 program_close_all_pipe(pipes, pipe_count); // 下面会执行程序，然后子进程会退出 // ... } //... pipe_index++; } // 最后再关闭一次 program_close_all_pipe(pipes, pipe_count); // 最后等待所有子进程完毕 waitpid(-child_pid, NULL, 0); 其实就是用管道把进程串起来的过程\n信号处理与终端控制 这个任务的关键是让shell本身不执行Ctrl-C，但子进程要能执行\n这里可以使用signal系统调用来注册信号处理函数\n当然这里也不用自己写，因为signal的处理是有默认实现的\n比如说signal(SIGINT, SIG_IGN);表示在收到SIGINT后忽略\n而signal(SIGINT, SIG_DFL);表示收到SIGINT后使用默认的实现，也就是退出进程\n需要注意的是，子进程默认会继承父进程的信号处理方式\n这里的shell需要忽略SIGINT之类的信号，但子进程不能\n所以需要在子进程创建后重新注册信号处理，让子进程使用默认实现\n// 主进程信号初始化 void signal_main_init(void) { signal(SIGINT, SIG_IGN); signal(SIGTSTP, SIG_IGN); signal(SIGTTOU, SIG_IGN); } // 子进程信号初始化 void signal_child_init(void) { signal(SIGINT, SIG_DFL); signal(SIGTSTP, SIG_DFL); signal(SIGTTOU, SIG_DFL); } 主要需要处理的是这三个信号，放到合适的位置即可\n然后就是进程组管理\n因为有些程序自己会创建很多子进程，我们希望一个SIGINT可以把整个程序杀掉\n所以最好就是子进程跟shell不在一个进程组里并且子进程是自己进程组的组长\n这样可以擒贼先擒王，子进程退出后，子进程下面的进程也就退出了\n通过setpgrp系统调用，可以将自己单独分出进程组并且自己是组长\n对于管道处理的情况，这一堆程序应当是一个进程组的\n// 子进程创建循环 for(...) { child_pid = fork(); if(child_pid == 0) { // ... // 下面会执行程序，然后子进程会退出 // ... } // 父进程设置子进程的进程组 if (pipe_index == 0) { setpgid(child_pid, child_pid); pgid = child_pid; } else { setpgid(child_pid, pgid); } // ... } 后台处理 这个部分比较神秘，涉及到前后台这个概念\n我的理解就是，前台程序拥有终端的输入输出，后台只能有终端的输出\n后台的程序还可能被暂停\n前台一般给到的是一个进程组\n通过tcsetpgrp这个系统调用，可以将终端前台权转让给一个进程组\n需要注意的是，使用tcsetpgrp转让前台权时，这个进程组要有前台权\ntcsetpgrp的第一个参数表示要转让的fd，一般转让标准输入\n第二个参数表示转让给的进程组ID\nbool is_background; for(...) { child_pid = fork(); if(child_pid == 0) { // ... // 这里的部分不重要 // 下面会执行程序，然后子进程会退出 // ... } // 父进程获取子进程组 if (!is_background) { // 前台给到子进程组 tcsetpgrp(STDIN_FILENO, pgid); } // ... } 前后台切换 这里需要添加新的类型，用来保存所有前后台的任务\n这个列表也用来切换进程的前后台状态\n// 添加job类型 typedef struct { // 记录pid pid_t pid; // 记录是否是后台任务 bool is_background; // 保存当前终端设置 struct termios termios; } job; 然后添加一系列函数用来控制job列表\nfg的任务是把一个后台任务拉到前台来\n// fg的核心函数 bool job_to_forground(job *job) { // 实现无PID输入拉起最近的任务 job = job == NULL ? recent_job : job; if (job-\u0026gt;is_background == false) { return false; } // 转让前台 tcsetpgrp(STDIN_FILENO, job-\u0026gt;pid); // 恢复termios tcsetattr(STDIN_FILENO, TCSADRAIN, \u0026amp;job-\u0026gt;termios); // 传递信号 kill(job-\u0026gt;pid, SIGCONT); job-\u0026gt;is_background = false; // 等待这个子进程结束 waitpid(job-\u0026gt;pid, NULL, WUNTRACED); // 前台回到shell tcsetpgrp(STDIN_FILENO, getpgrp()); return true; } bg的任务是将后台任务从暂停中恢复\n// bg的核心函数 bool job_resume(job *job) { job = job == NULL ? recent_job : job; // 传递信号 kill(job-\u0026gt;pid, SIGCONT); job-\u0026gt;is_background = true; return true; } // 本身就是后台任务，也就没必要等待了 \u0026lt;0x03\u0026gt; 后记 首先感谢你能看到这里，写得还是挺啰嗦挺碎的\n整个的代码我折腾了两天时间\n其实实现这个东西还是挺有成就感的\n有一说一，通过写一个shell来入门系统调用确实是不错的选择\nshell可以说大部分功能就是靠系统调用实现的\n而且也大多是常用的系统调用\n如果没这项目，我还真不知道写什么可以很好入门系统调用\nshell的大部分功能跟sh比较过，行为上基本是一致的\n（就除了几个回显消息不同，不过这个项目没规定回显消息格式）\n但学校的测试机不知道有什么问题，我最高跑到了9/10\n很神秘，甚至我直接在GitHub上找了个代码直接贴上去也是9/10\n（那份代码完成到后台处理，但测试机也判定后台处理不通过）\n疑似是测试机的问题\n经过学长验证，这个测试的最高分疑似就是9\n而且拿到9分都不用写这么多，只要写到文件重定向就能拿到了\n那我不就很小丑吗，我的实现在Berkeley都可以拿额外学分了\n可惜工地终究不是它\n这也验证了学校的测试机是仿制的\n","date":"2024-12-02T14:24:43+08:00","permalink":"/post/categories/cs162/zjut-os-02-%E8%AE%B0%E5%BD%95/","title":"Zjut-OS-02 Shell 记录"},{"content":"\u0026lt;0x01\u0026gt; 任务说明 更高效的线程睡眠 目前的timer_sleep实现是忙等待的\n这并不高效\n严格优先级调度 在pintos中，每个线程的优先级有0(PRI _ MIN)到63 (PRI _ MAX)\n但是，当前计划程序不尊重这些优先级值\n您必须修改调度程序，使高优先级线程始终在低优先级线程之前运行\n注意：在优先级相同的情况下，应该以循环方式调度具有相同优先级值的线程\n现有代码已经在线程运行超过分配的时间片时抢占了线程\n因此您不必担心循环实现的这一部分\n如果您想更仔细地了解这是如何实现的，那么可以查看thread_tick\n您还必须修改3个pintos同步原语\nlock、信号量、条件变量\n以便这些共享资源更喜欢优先级较高的线程，而不是优先级较低的线程\n此外，您必须实现优先级借用机制\n当高优先级线程(A)不得不等待获取一个已经被低优先级线程(B)持有的锁时，我们暂时将B的优先级提高到A的优先级。不提供优先级的调度程序容易出现优先转置1的问题，即中等优先级的线程在运行，而高优先级的线程 (a) 在等待由低优先级线程 (b) 持有的资源。支持优先级捐赠的调度程序将允许 B 先运行，这样具有最高优先级的 A 可以被解除阻塞。执行优先捐赠必须处理以下事项： 1) 来自多个来源的捐赠；2) 解锁后撤销捐赠；3) 嵌套 / 递归捐赠。\n线程可以调用thread_set_priority设置自己的优先级\n也可以调用thread_get_priority获得自己的优先级。\n如果一个线程不再拥有最高的“有效优先级”\n它必须立即将 CPU 交给最高优先级的线程。\n实际的优先级调度程序本身并不需要太多的复杂性\n如果您对如何有效地实现这种调度程序感到困惑\n请考虑现有的操作系统如何实现这种调度程序。\n不要忘记实现thread_get_priority，这是返回当前线程优先级的函数\n在考虑优先级借用机制是，应该返回线程的有效优先级\n一个线程不能改变另一个线程的优先级，除非通过优先级借用\n如果一个线程不再具有最高的有效优先级 (例如，因为它释放了一个锁或者它用一个较低的值调用 thread _ set _  优先级) ，它必须立即生成 CPU。如果释放了一个锁，但是当前线程仍然具有最高的有效优先级，那么它不应该产生 CPU。\n这项任务的优先捐赠部分可能需要一些思考ーー在纸上或白板上描绘一些场景，看看你提议的系统是否有效，可能会有所帮助。\n您只需实现锁的优先级捐赠。不要对其他同步变量实现它们 (对信号量或监视器这样做没有任何意义，因为，例如，没有信号量唯一 “所有者” 的概念)。但是，您需要为锁、信号量和条件变量实现优先级调度。优先级调度是当释放资源或发出监视器信号时解除阻塞最高优先级线程的情况。\n一个线程一次只能 (直接) 捐赠给一个线程，因为一旦它调用 lock _ access，捐赠者线程就会被阻塞。\n您的实现必须处理嵌套捐赠： 考虑一个高优先级线程 H、一个中优先级线程 M 和一个低优先级线程 L。如果 H 必须等待 M，M 必须等待 L，那么我们应该将 H 的优先级赋予 L。\n如果在调用 lock _ release 时，一个锁上有多个服务器，那么所有这些优先级捐赠必须应用于接收下一个锁的线程。\n不需要为用户线程增加调度程序；只需让调度程序以相同的方式对待所有线程，即使它们属于同一个进程。作为一个病态的例子，如果一个用户程序 A 有 100 个线程，而一个用户程序 B 只有 1 个线程，那么大部分的 CPU 将被 A 的线程所主宰，而 B 的线程将处于饥饿状态。您不需要增加调度程序来使这个场景更加公平。\n这个优先转置的具体例子并不是优先转置发生的唯一途径。优先转置就是高优先级的线程被低优先级的线程阻止运行。↩\n用户级线程 pintos 是一个多线程内核 (也就是说，可以有多个线程在内核中运行)\n在使用userprog时，您无疑使用了内核中的线程接口\n在 thread/thread.c 中，thread_create 允许我们创建一个运行特定内核任务的新内核线程\n类似地，thread_exit 函数允许线程终止自己。您应该阅读并理解内核线程模型。\n另一方面，正如在userprog中一样，每个用户进程只有一个控制线程\n换句话说，用户程序不可能创建一个新线程来运行另一个用户函数\n用户程序没有执行 thread_create 和 thread_exit 的能力\n但在真实的系统中，用户程序可以创建自己的线程。比如通过 POSIX pthread 库。\n对于此任务，需要在 lib/user/pthread.h 中实现 pthread 库的简化版本\n应该允许用户程序使用 pthread_create 和 pthread_exit 函数创建自己的线程\n线程还可以使用 pthread_join 函数等待其他线程，这类似于等待进程的系统调用\n线程应该能够通过一个新的 get_tid 系统调用学习它们的线程 ID (TID)\n还需要说明使用户程序成为多线程对项目用户程序中的系统调用的影响。\n在userprog中，每当一个用户程序 (只包含一个线程) 在操作系统中时，它都会在自己的专用内核线程中运行\n换句话说，用户线程与内核线程之间有 1-1 的映射\n对于这个任务，您将需要维护这个 1-1 映射。也就是说，具有 n 个用户线程的用户进程应该与 n 个内核线程 1-1 配对，每个用户线程在陷入操作系统时应该在其专用的内核线程中运行。您不应该实现绿色线程，绿色线程在用户线程和内核线程之间有多对一的映射。绿色线程并不理想，因为一旦一个用户线程被阻塞，例如在 IO 上，所有的用户线程都会被阻塞。\n此外，还必须实现用户级同步。毕竟，如果我们不能用锁和信号量正确地同步线程，那么线程就没有多大用处。您需要为用户程序实现 lock_init、 lock_access、 lock_release、 sema_init、 sema_down 和 sema_up。\n工作流建议： 这个任务最容易在小步骤中完成。首先实现一个简单的 pthread_create 和 pthread_execute，这样就可以通过 test/userprog/multithread/create-simple。然后，慢慢地添加越来越多的特性。扩充一个工作设计比修复一个坏的设计更容易。确保仔细跟踪资源。您分配的所有东西都必须被释放！\n实现需求 新的系统调用 需要修改的系统调用 同步 实现提示 额外信息 pthread库 threading 用户级的同步 \u0026lt;0x02\u0026gt; 代码实现 更高效的线程睡眠 这个相对简单\n原本的timer_sleep实现是这样的\nvoid timer_sleep(int64_t ticks) { int64_t start = timer_ticks(); ASSERT(intr_get_level() == INTR_ON); while (timer_elapsed(start) \u0026lt; ticks) thread_yield(); } 可以看到就是跑了个死循环\n线程还是在就绪和运行态中切换，我们需要让它进入阻塞态\n首先先添加一个休眠线程队列（记得初始化）\n// 存放睡眠队列 static struct list sleep_thread_list; 然后要给线程加上相应的休眠信息\nstruct thread { // ... int64_t sleep_tick; // 睡眠终止时间刻 // ... }; 现在开始实现线程休眠的逻辑\n首先就是要先想办法让线程休眠\n这个相对简单，先更新线程的休眠终止时间，然后让线程进入阻塞态即可\n// 修改原来的忙等待实现 void timer_sleep(int64_t ticks) { int64_t start = timer_ticks(); struct thread *cur = thread_current(); enum intr_level old_level = intr_disable(); cur-\u0026gt;sleep_tick = start + ticks; // 加入睡眠队列 list_push_back(\u0026amp;sleep_thread_list, \u0026amp;cur-\u0026gt;sleep_elem); thread_block(); intr_set_level(old_level); } 现在线程休眠了，那么怎么醒来呢\n在系统中，有一个时钟中断函数timer_interrupt\n系统每过一个tick，就会执行这个函数\n我们的苏醒逻辑就添加在这里\nstatic void timer_interrupt(struct intr_frame *args UNUSED) { ticks++; enum intr_level old_level = intr_disable(); struct list_elem *e = list_begin(\u0026amp;sleep_thread_list); while (e != list_end(\u0026amp;sleep_thread_list)) { struct thread *t = list_entry(e, struct thread, sleep_elem); if (t-\u0026gt;sleep_tick \u0026lt;= ticks) { t-\u0026gt;sleep_tick = 0; thread_unblock(t); e = list_remove(e); } else e = list_next(e); } intr_set_level(old_level); thread_tick(); } 这样就可以了\n严格优先级调度 首先先添加按优先级的调度\n// 优先级调度的列表 static struct list prio_ready_list; // ... static struct thread *thread_schedule_prio(void) { if (!list_empty(\u0026amp;prio_ready_list)) { list_sort(\u0026amp;prio_ready_list, thread_cmp_priority_more, NULL); return list_entry(list_pop_front(\u0026amp;prio_ready_list), struct thread, elem); } else { return idle_thread; } } 然后处理新建线程时新线程优先级更高的情况\ntid_t thread_create(const char *name, int priority, thread_func *function, void *aux) { // ... // 新创建的线程优先级更高就让步 if (active_sched_policy == SCHED_PRIO \u0026amp;\u0026amp; t-\u0026gt;priority \u0026gt; thread_current()-\u0026gt;priority) { thread_yield(); } // ... } 下面处理优先级借用的问题\n首先先对thread的数据结构进行更改\nstruct thread { // ... int priority; /* Priority. */ int origin_priority; // 原始优先级 struct list donation_list; // 借用队列 struct lock *blocking_lock; // 被阻塞的锁 // ... }; struct thread_donation_node { struct list_elem elem; struct lock *lock; struct thread *donor; int priority; }; 优先级借用队列信息保存在thread中\n这个问题的处理关键是让一系列锁同步的线程都具有链条上的最高优先级\n所以可以做出下面的更改\nstatic void lock_update_blocking_priority(struct thread *t) { if (t-\u0026gt;blocking_lock == NULL) { return; } struct thread *holder = t-\u0026gt;blocking_lock-\u0026gt;holder; ASSERT(holder != NULL); for (struct list_elem *e = list_begin(\u0026amp;holder-\u0026gt;donation_list); e != list_end(\u0026amp;holder-\u0026gt;donation_list);) { struct thread_donation_node *node = list_entry(e, struct thread_donation_node, elem); e = list_next(e); if (node-\u0026gt;lock == t-\u0026gt;blocking_lock) { node-\u0026gt;priority = t-\u0026gt;priority; if (holder-\u0026gt;priority \u0026lt; t-\u0026gt;priority) { holder-\u0026gt;priority = t-\u0026gt;priority; lock_update_blocking_priority(holder); } return; } } } static void lock_holder_insert_donation(struct thread *cur, struct lock *lock) { if (lock-\u0026gt;holder != NULL \u0026amp;\u0026amp; lock-\u0026gt;holder-\u0026gt;priority \u0026lt; cur-\u0026gt;priority) { struct thread_donation_node *node = malloc(sizeof(struct thread_donation_node)); node-\u0026gt;lock = lock; node-\u0026gt;donor = cur; node-\u0026gt;priority = cur-\u0026gt;priority; list_push_back(\u0026amp;lock-\u0026gt;holder-\u0026gt;donation_list, \u0026amp;node-\u0026gt;elem); lock-\u0026gt;holder-\u0026gt;priority = cur-\u0026gt;priority; lock_update_blocking_priority(lock-\u0026gt;holder); } } 然后在合适的位置调用即可\n其他的同步设施更改就不是很关键了，只要确保优先调用高优先级的即可\n用户级线程 这个相对困难，又涉及到一些汇报和内存位置问题\n首先，pthread没有load函数中那样，自动帮你把一下内存位置设置好\n不过参考前面的函数，也可以写出\nbool setup_thread(void **esp) { uint8_t *kpage; bool success = false; enum intr_level old_level = intr_disable(); struct thread *t = thread_current(); if (t-\u0026gt;pcb != NULL \u0026amp;\u0026amp; t-\u0026gt;pcb-\u0026gt;pagedir != NULL) { // 获取一个新的内存页，初始为0 kpage = palloc_get_page(PAL_USER | PAL_ZERO); if (kpage != NULL) { void *base = PHYS_BASE; int max_pthread_num = 1000000; // 寻找空闲的页 for (int i = 0; i \u0026lt; max_pthread_num; i++) { base -= PGSIZE; if (!pagedir_is_accessed(t-\u0026gt;pcb-\u0026gt;pagedir, (uint8_t *)base - PGSIZE)) { break; } } if (t-\u0026gt;pcb-\u0026gt;pagedir != NULL) { // 将新分配的内存页安装到页目录中，确保线程可以访问这段内存。 success = install_page(((uint8_t *)base) - PGSIZE, kpage, true); } if (success) { t-\u0026gt;upage = ((uint8_t *)base) - PGSIZE; *esp = base; } else { palloc_free_page(kpage); } } } intr_set_level(old_level); return success; } 剩下的工作跟userprog中差不多\n按功能要求即可，注意资源的释放\n\u0026lt;0x03\u0026gt; 测试相关 如果跟我一样是手搓的环境，并且没搓bochs环境的话\n跑测试会有一些小问题\n首先是threads部分的测试默认都是在bochs下面跑的\n如果要用qemu跑，那么就要改makefile\n# /tests/threads/Make.tests # 90行左右 # tests/threads/%.output: SIMULATOR = --bochs tests/threads/%.output: SIMULATOR = --qemu 然后就是qemu的运行速度会比较慢，在部分测试中会超时\n但如果单独运行的话会发现结果还是正确的\n(就是耗时超长)\n由于测试机会读项目的makefile，所以提交时最好改回来\n还有一个办法，就是改计时器频率\n也就是devices/timer.h里面的TIMER_FREQ\n这个值默认是100，意味着pintos每秒100个时钟周期\n虽然正常情况下是不要动这个东西，但这里可以暂时改成20000\n然后吧devices/timer.c的这个注释掉\n#if TIMER_FREQ \u0026lt; 19 #error 8254 timer requires TIMER_FREQ \u0026gt;= 19 #endif #if TIMER_FREQ \u0026gt; 1000 // #error TIMER_FREQ \u0026lt;= 1000 recommended #endif 如果测试功能的话可以暂时开启，但不知道会出什么别的问题\n","date":"2024-12-02T04:19:58+08:00","permalink":"/post/categories/cs162/zjut-os-04-%E8%AE%B0%E5%BD%95/","title":"Zjut-OS-04 Threads 记录"},{"content":"这里主要讲解思路，代码可能展示得不会很详细\n完整代码在Github\n\u0026lt;0x01\u0026gt; Task 1: Alarm Clock 任务摘要 这个任务要求重新实现./devices/timer.c的timer_sleep()函数\n原本的timer_sleep()实现是基于无限循环直到到达等待时间的忙等待\n我们需要避免这个忙等待\n怎么实现 参考5状态模型，线程运行部分主要有三个状态\n分别是运行态、准备态和阻塞态\n原本的忙实现仅在运行态和准备态之间切换\n所以这里我们要做的就是把阻塞态用上\n所以可以单独开一个线程队列，用来存放所有休眠的线程\n然后在每次时钟中断时检查休眠线程队列，把到时间的线程放到准备队列中即可\n代码实现 首先先看下原始代码\nvoid timer_sleep(int64_t ticks) { int64_t start = timer_ticks(); enum intr_level old_level; ASSERT(intr_get_level() == INTR_ON); while (timer_elapsed(start) \u0026lt; ticks) thread_yield(); } 可以看到就是记录了开始的时间刻，然后无限循环\n无限循环就是我们要删除的\n添加休眠线程队列 在./threads/thread.c的最上面，有定义ready_list的部分\n按这个格式再定义一个sleep_list作为休眠线程队列\nstatic struct list sleep_list; 定义完后往下找，会找到一个thread_init函数，这里初始化了ready_list\n我们也跟着初始化一下\nvoid thread_init(void) { ASSERT(intr_get_level() == INTR_OFF); lock_init(\u0026amp;tid_lock); list_init(\u0026amp;ready_list); list_init(\u0026amp;all_list); // lab1 初始化休眠线程队列 list_init(\u0026amp;sleep_list); /* Set up a thread structure for the running thread. */ initial_thread = running_thread(); init_thread(initial_thread, \u0026#34;main\u0026#34;, PRI_DEFAULT); initial_thread-\u0026gt;status = THREAD_RUNNING; initial_thread-\u0026gt;tid = allocate_tid(); } 给线程加上苏醒时间信息 在tcb中加上线程的休眠时间信息，thread.h中添加\nstruct thread { /* Owned by thread.c. */ tid_t tid; /* Thread identifier. */ enum thread_status status; /* Thread state. */ char name[16]; /* Name (for debugging purposes). */ uint8_t *stack; /* Saved stack pointer. */ int priority; /* Priority. */ struct list_elem allelem; /* List element for all threads list. */ int64_t wakeup_ticks; // lab1 添加苏醒时间刻 /* Shared between thread.c and synch.c. */ struct list_elem elem; /* List element. */ #ifdef USERPROG /* Owned by userprog/process.c. */ uint32_t *pagedir; /* Page directory. */ #endif /* Owned by thread.c. */ unsigned magic; /* Detects stack overflow. */ }; 这个信息用来给内核检查是否到达对应的时间，然后做相应的操作\n添加线程休眠逻辑 现在有休眠队列了，在线程需要休眠的时候将其添加到这个队列中\n// 用来比较线程等待至目标时间刻的函数 bool thread_less_wakeup_tick(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED) { struct thread *p_thread_a = list_entry(a, struct thread, elem); struct thread *p_thread_b = list_entry(b, struct thread, elem); return p_thread_a-\u0026gt;wakeup_ticks \u0026lt; p_thread_b-\u0026gt;wakeup_ticks; } // 这个记得在thread.h中声明，要在timer.c中用的 void thread_sleep(int64_t target_ticks) { // 获取当前线程 struct thread *cur = thread_current();; // 给当前线程添加苏醒时间刻 cur-\u0026gt;wakeup_ticks = target_ticks; // 按苏醒时间刻从小到大插入休眠队列 list_insert_ordered(\u0026amp;sleep_list, \u0026amp;cur-\u0026gt;elem, thread_less_wakeup_tick, NULL); // 当前线程进入阻塞态 thread_block(); } 然后在timer_sleep中调用即可\nvoid timer_sleep(int64_t ticks) { enum intr_level old_level; ASSERT(intr_get_level() == INTR_ON); old_level = intr_disable(); thread_sleep(timer_ticks() + ticks); intr_set_level(old_level); } 除了thread_sleep的代码跟中断处理有关系，我还没摸透\n添加线程苏醒逻辑 现在线程会休眠了，但线程自己不会苏醒，需要内核操作\n既然跟时间相关，那么就应该在时钟中断里面做点什么\n也就是./devices/timer.c的timer_interrupt函数\nstatic void timer_interrupt(struct intr_frame *args UNUSED) { ticks++; thread_tick(); // lab1 将到达休眠时间将线程移出休眠队列 thread_wakeup(); } 这里的thread_wakeup是需要在./threads/thread.c中实现的函数\n// 记得添加声明到thread.h中 void thread_wakeup(void) { struct list_elem *p_elem; struct thread *p_thread; while (!list_empty(\u0026amp;sleep_list)) { p_elem = list_front(\u0026amp;sleep_list); p_thread = list_entry(pe, struct thread, elem); // 休眠队列本身就是从小到大排序的，可以这样提前退出 if (p_thread-\u0026gt;wakeup_ticks \u0026gt; timer_ticks()) { break; } // 移除出休眠队列 list_remove(p_elem); // 让pt指向的线程退出阻塞态进入准备态 thread_unblock(p_thread); } } 运行结果 运行测试alarm-multiple\n(alarm-multiple) begin\r(alarm-multiple) Creating 5 threads to sleep 7 times each.\r(alarm-multiple) Thread 0 sleeps 10 ticks each time,\r(alarm-multiple) thread 1 sleeps 20 ticks each time, and so on.\r(alarm-multiple) If successful, product of iteration count and\r(alarm-multiple) sleep duration will appear in nondescending order.\r(alarm-multiple) thread 0: duration=10, iteration=1, product=10\r(alarm-multiple) thread 1: duration=20, iteration=1, product=20\r(alarm-multiple) thread 0: duration=10, iteration=2, product=20\r(alarm-multiple) thread 2: duration=30, iteration=1, product=30\r(alarm-multiple) thread 0: duration=10, iteration=3, product=30\r(alarm-multiple) thread 3: duration=40, iteration=1, product=40\r(alarm-multiple) thread 1: duration=20, iteration=2, product=40\r(alarm-multiple) thread 0: duration=10, iteration=4, product=40\r(alarm-multiple) thread 4: duration=50, iteration=1, product=50\r(alarm-multiple) thread 0: duration=10, iteration=5, product=50\r(alarm-multiple) thread 2: duration=30, iteration=2, product=60\r(alarm-multiple) thread 1: duration=20, iteration=3, product=60\r(alarm-multiple) thread 0: duration=10, iteration=6, product=60\r(alarm-multiple) thread 0: duration=10, iteration=7, product=70\r(alarm-multiple) thread 3: duration=40, iteration=2, product=80\r(alarm-multiple) thread 1: duration=20, iteration=4, product=80\r(alarm-multiple) thread 2: duration=30, iteration=3, product=90\r(alarm-multiple) thread 4: duration=50, iteration=2, product=100\r(alarm-multiple) thread 1: duration=20, iteration=5, product=100\r(alarm-multiple) thread 3: duration=40, iteration=3, product=120\r(alarm-multiple) thread 2: duration=30, iteration=4, product=120\r(alarm-multiple) thread 1: duration=20, iteration=6, product=120\r(alarm-multiple) thread 1: duration=20, iteration=7, product=140\r(alarm-multiple) thread 4: duration=50, iteration=3, product=150\r(alarm-multiple) thread 2: duration=30, iteration=5, product=150\r(alarm-multiple) thread 3: duration=40, iteration=4, product=160\r(alarm-multiple) thread 2: duration=30, iteration=6, product=180\r(alarm-multiple) thread 4: duration=50, iteration=4, product=200\r(alarm-multiple) thread 3: duration=40, iteration=5, product=200\r(alarm-multiple) thread 2: duration=30, iteration=7, product=210\r(alarm-multiple) thread 3: duration=40, iteration=6, product=240\r(alarm-multiple) thread 4: duration=50, iteration=5, product=250\r(alarm-multiple) thread 3: duration=40, iteration=7, product=280\r(alarm-multiple) thread 4: duration=50, iteration=6, product=300\r(alarm-multiple) thread 4: duration=50, iteration=7, product=350\r(alarm-multiple) end 当然，有人要说了，这样看不出来跟忙等待实现的差别啊\n这里就可以在./threads/init.c的main函数中加点东西\n// main函数，东西太多就不贴了 // ... printf(\u0026#34;Boot complete.\\n\u0026#34;); /* Run actions specified on kernel command line. */ run_actions(argv); // 加上这句 thread_print_stats(); /* Finish up. */ shutdown(); thread_exit(); // ... 然后执行，看最后的一句输出\n忙等待实现是这样的\nThread: 0 idle ticks, 592 kernel ticks, 0 user ticks 休眠队列的实现是这样的\nThread: 550 idle ticks, 40 kernel ticks, 0 user ticks 可以看到忙等待实现不会跑idle\n需要注意的是，虽然alarm-priority在这个部分，但这个测试目前是过不了的\n要到task2写完才可以\n\u0026lt;0x02\u0026gt; Task2: Priority Scheduling 测试目标为[Lab1] Default Scheduler Debug\n任务摘要 优先级调度器不会在之后的项目中使用\nTask2.1 实现优先级调度 当一个线程加入到就绪队列时，如果这个新线程的优先级高于当前线程，当前线程应该立即让步\n同样的，如果线程正在等待锁、信号或者条件时，高优先级的线程应当最先唤醒\n线程自己可以改变优先级，但如果优先级改变后，自己不具有最高优先级，应当立即让步\nTask2.2 优先级反转问题 考虑三个线程，分别有高中低的优先级\n低优先级线程占有一个锁，然后这时候高优先级线程也想占用这个锁\n那么高优先级线程会被阻塞，等待低优先级线程释放这个锁\n但如果后面中优先级的线程启动了，那么由于优先级确实比低优先级高\n中优先级线程会在低优先级线程时钟中断后执行，并且直到处理完毕\n本来应该最优先处理的高优先级线程实际上没有最优先处理\n这个现象称为优先级反转\npriority\r↑\r│ ↓ 高优先级线程尝试获取锁，但会阻塞\r│ │─│ │ high ──────────│\r│ ↓ 中优先级启动 ↑ 高优先级反而最后处理\r│ │ mid ─────────────│\r│ ↓ 低优先级启动\r│ │ low ─────│ │ low ──│\r│ ↑ 低优先级时间片耗尽\r└────────────────────────────────────────────────────────────→ time 所以需要实现优先级借用机制来解决这个问题\n（原词是priority donation，我觉得翻译成优先级借用比较合适）\n优先级借用就是如果有高优先级线程尝试获取锁\n那么原来低优先级线程暂时获得高优先级线程的优先级\n还是前面例子，如果使用优先级借用机制，那么优先级与时间关系图将会时这样\npriority\r↑\r│ ↓ 尝试获取锁 ↓ 低优先级线程处理完毕后马上处理高优先级\r│ │─││ low ──────││ high ──────────│\r│ ↑ 低优先级线程优先级暂时提升 ↓ 中优先级启动\r│ │ mid ───────────│\r│ ↓ 低优先级启动\r│ │ low ─│\r│\r└────────────────────────────────────────────────────────────→ time 低优先级线程优先级会暂时提升到高优先级线程的水平\n实现低优先级处理完就处理高优先级线程任务\n你需要考虑所有的优先级借用的情况\n必须实现锁的优先级借用，其他的同步结构的优先级借用不必实现\n你需要实现所有情况的优先级调度\n确保多个优先级借用时的工作情况\n支持嵌套优先级借用 还是一样的优先级借用，但是需要考虑嵌套时的优先级借用的传递问题\nTask2.3 实现优先级的获取与修改 这个任务需要你修改thread_set_priority和thread_get_priority 对于thread_set_priority，如果新优先级不具有最高的优先级，那么当前线程应当让步\n对于thread_get_priority，返回当前优先级或者说借用的优先级\npintos默认实现的东西 pintos虽然没有实现优先级调度\n但已经提供了一套优先级\n范围0到63，线程默认优先级为31\n调度没做高优先级优先调度的功能，是先进先出的\n在信号量、锁、条件这些同步设施上，pintos的实现也是最基本的\n只是能用，也都没做优先级处理的操作\n代码实现 Task2.1 这部分还是简单的\n先在./threads/thread.c实现一个线程优先级比较函数\n// lab1 线程优先级比较函数 bool thread_more_priority(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED) { struct thread *p_thread_a = list_entry(a, struct thread, elem); struct thread *p_thread_b = list_entry(b, struct thread, elem); return p_thread_a-\u0026gt;priority \u0026gt; p_thread_b-\u0026gt;priority; } 修改的是thread_create函数\nTask2.2 这个可以说是这个lab最难的部分\n如果lock的部分没写好，main线程初始化都会出问题，调试都不好调试\n所以很多时候都不知道自己哪里错了\nTask2.3 thread_get_priority非常简单，直接返回当前线程优先级\nint thread_get_priority(void) { return thread_current()-\u0026gt;priority; } thread_set_priority也很简单，只是要处理一下锁的优先级借用问题\n// lab1 当前线程是否持有锁 bool thread_is_holding_lock(void) { struct thread *cur = thread_current(); return !list_empty(\u0026amp;cur-\u0026gt;lock_list); } void thread_set_priority(int new_priority) { // 断言限制优先级范围 ASSERT(PRI_MIN \u0026lt;= new_priority \u0026amp;\u0026amp; new_priority \u0026lt;= PRI_MAX); struct thread *cur = thread_current(); cur-\u0026gt;base_priority = new_priority; if (!thread_is_holding_lock() || new_priority \u0026gt; cur-\u0026gt;priority) { cur-\u0026gt;priority = new_priority; thread_yield(); } } 如果没持有锁，那么直接更新就可以，如果有锁，那么还要判断下是否大于当前优先级\n基础优先级是一定会更新的\n\u0026lt;0x03\u0026gt; Task3: Advanced Scheduler 测试目标为[Lab1] Advanced Scheduler Debug\n任务摘要 实现附录中的BSD调度\n高级调度器不会在之后的项目中使用\nBSD调度 那么什么是BSD调度呢\n前面我们实现的调度是基于优先级的，线程需要根据自己的情况给内核一个优先级\n但这样不够智能，更何况如果有恶意线程啥都不干但占用最高优先级，那不就完了\nBSD调度算是一种优先级计算方法，它会评估系统总体的占用负载和线程的cpu使用率\n然后给线程一个合理的优先级\n整个BSD调度中线程需要新增两个量，niceness和recent_cpu\nniceness表示线程的好心程度，范围是-20~20，默认值为0\nniceness越大，表示这个线程越好心，越愿意让步主动权\n相反，如果这个值越小，表示这个线程不希望让步\nrecent_cpu表示这个线程的cpu使用频率，没啥好说的\nBSD调度的优先级计算方法如下，每4个时钟中断更新一次 $$priority=PRI_MAX-\\frac{recent_cpu}{4}-2niceness$$ 其中，recent_cpu的更新方法如下\n每个时钟中断时，recent_cpu自增1\n每过了物理时间1s，recent_cpu按下面公式重新计算\n$$recent_cpu=\\frac{2load_avg}{2*load_avg+1}*recent_cpu+niceness$$ 这里出现的load_avg表示系统负载水平，计算公式如下，每物理时间1s更新一次\n$$load_avg=\\frac{59}{60}*load_avg+\\frac{1}{60}*ready_threads$$ ready_threads表示不包括idle的运行线程和就绪线程的个数\n（这里省略了很多公式推导的细节）\n代码实现 这个还是比较简单的，基本上按着文档走就可以\n实现模拟浮点库 pintos本身没有浮点类型，但BSD调度中会用到小数\n好在文档的后面提供了一个方案，可以用模拟的浮点数\n这个就按文档最后实现一下就好了\n在我的代码中，实现的文件是./threads/fp.h和./threads/fp.c\n这里就不贴了，挺重复的\n需要指出的是，我使用了typedef int fp;，提示自己是浮点数\n禁用优先级借用机制 文档中有提到，BSD调度器不需要优先级借用机制\n所以就把它禁用掉就可以了\n在测试这个任务的代码时，会传入-mlfqs这个参数\n这个参数会使thread_mlfqs变为true\n通过这个值可以判断是否测试的是高级调度器\nvoid lock_acquire(struct lock *lock) { ASSERT(lock != NULL); ASSERT(!intr_context()); ASSERT(!lock_held_by_current_thread(lock)); // lab1 高级调度不需要优先级借用 if (!thread_mlfqs) { lock_acquire_priority_donation(lock); } sema_down(\u0026amp;lock-\u0026gt;semaphore); lock-\u0026gt;holder = thread_current(); } void lock_release(struct lock *lock) { ASSERT(lock != NULL); ASSERT(lock_held_by_current_thread(lock)); // lab1 高级调度不需要优先级借用 if (!thread_mlfqs) { lock_release_priority_donation(lock); } lock-\u0026gt;holder = NULL; sema_up(\u0026amp;lock-\u0026gt;semaphore); } lock_acquire_priority_donation和lock_release_priority_donation就是前面实现的优先级借用方法\n只是把这两个封装成单独的函数而已\n修改thread相关的代码 在./threads/thread.h中添加niceness的范围与默认值定义\n// lab1 高级调度 niceness部分 #define NICENESS_MIN -20 #define NICENESS_DEFAULT 0 #define NICENESS_MAX 20 添加load_avg的定义（在./threads/thread.c中也加一个）\n// lab1 高级调度 系统负载 extern fp thread_load_avg; 给线程控制块加上niceness和recent_cpu这两个值\nstruct thread { // ... int niceness; // lab1 高级调度 线程的好心程度 fp recent_cpu; // lab1 高级调度 线程使用的cpu时间 // ... }; 实现BSD调度的公式计算 因为这个调度的值都是线程相关的，所以相关的计算我也就放在./threads/thread.c里面了\n后面./devices/timer.c也会用到这几个函数，所以要添加声明到对应头文件中\n// lab1 高级调度 基于nice与recent_cpu计算线程优先级 void thread_mlfqs_refresh_priority(struct thread *t) { if (t == idle_thread) { return; } fp f_priority = fp_convert_from(PRI_MAX); // 1/4 * recent_cpu fp f_a = fp_div_int(t-\u0026gt;recent_cpu, 4); // 2*niceness fp f_b = fp_convert_from(t-\u0026gt;niceness); f_b = fp_mul_int(f_b, 2); // PRI_MAX - 1/4 * recent_cpu - 2 * niceness fp f_ans = fp_sub_fp(f_priority, f_a); f_ans = fp_sub_fp(f_ans, f_b); t-\u0026gt;priority = fp_toint_round(f_ans); } // lab1 高级调度 当前线程recent_cpu自增1 void thread_mlfqs_increase_recent_cpu(void) { struct thread *cur = thread_current(); if (cur == idle_thread) { return; } cur-\u0026gt;recent_cpu = fp_add_int(cur-\u0026gt;recent_cpu, 1); } // lab1 高级调度 计算线程最近cpu使用率 void thread_mlfqs_refresh_recent_cpu(struct thread *t) { if (t == idle_thread) { return; } // (2 * load_avg) / (2 * laod_avg + 1) * recent_cpu fp f_a = fp_mul_int(thread_load_avg, 2); f_a = fp_div_fp(f_a, fp_add_int(f_a, 1)); f_a = fp_mul_fp(f_a, t-\u0026gt;recent_cpu); fp f_niceness = fp_convert_from(t-\u0026gt;niceness); // (2 * load_avg) / (2 * laod_avg + 1) * recent_cpu + niceness t-\u0026gt;recent_cpu = fp_add_fp(f_a, f_niceness); } // lab1 高级调度 计算系统使用负载 void thread_mlfqs_refresh_load_avg(void) { // 59/60 * load_avg fp f_a = fp_convert_from(59); f_a = fp_div_int(f_a, 60); f_a = fp_mul_fp(f_a, thread_load_avg); // 1/60 * ready_threads fp f_b = fp_convert_from(1); f_b = fp_div_int(f_b, 60); f_b = fp_mul_int(f_b, list_size(\u0026amp;all_list) - list_size(\u0026amp;sleep_list) - 1); // 59/60 * load_avg + 1/60 * ready_threads thread_load_avg = fp_add_fp(f_a, f_b); } 修改时钟中断行为 毕竟是涉及到时间的操作，相关代码修改在./devices/timer.c中完成\nstatic void timer_interrupt(struct intr_frame *args UNUSED) { ticks++; thread_tick(); // lab1 将到达休眠时间将线程移出休眠队列 thread_wakeup(); // lab1 高级调度 if (thread_mlfqs) { // 每tick增加当前线程recent_cpu thread_mlfqs_increase_recent_cpu(); if (timer_ticks() % TIMER_FREQ == 0) { // 每1s重新计算系统负载和所有线程的recent_cpu thread_mlfqs_refresh_load_avg(); thread_foreach(thread_mlfqs_refresh_recent_cpu, NULL); } if (timer_ticks() % 4 == 0) { // 每4个tick重新计算所有线程的优先级 thread_foreach(thread_mlfqs_refresh_priority, NULL); } } } 以上完成了所有修改，并且所有的测试也都可以过了\n这部分的测试耗时很长，这个是正常的\n","date":"2024-10-31T12:33:16+08:00","permalink":"/post/categories/cs162/pintos-lab1%E8%AE%B0%E5%BD%95/","title":"Pintos-Lab1记录"},{"content":"归纳一下windows常安装的东西\n我目前算是包管理重度依赖，基本上所有的软件都靠包管理器管理\n主要用winget和scoop\nwinget部分 配置全局安装行为 我的习惯让winget的安装都在全局里\n这个不关键，只是方便我找软件的目录\n打开winget的json配置文件\nwinget settings 加上installBehavior还有里面的东西\n{ \u0026#34;$schema\u0026#34;: \u0026#34;https://aka.ms/winget-settings.schema.json\u0026#34;, \u0026#34;installBehavior\u0026#34;: { \u0026#34;preferences\u0026#34;: { \u0026#34;scope\u0026#34;: \u0026#34;machine\u0026#34; } } } 实用工具 # 新的powershell好用，包括历史命令之类的 winget install Microsoft.PowerShell # windows实用小工具合集，又名windows版本前瞻 winget install Microsoft.PowerToys # 最好的windows终端 winget install Microsoft.WindowsTerminal # 众所周知，windows是linux最好的发新版（确信 winget install Microsoft.WSL # 高级进程查看 winget install Microsoft.Sysinternals.ProcessExplorer # 好用的IDE winget install JetBrains.Rider 日常软件 # 思题目学习平台 winget install Valve.Steam # 网易云 winget install NetEase.CloudMusic 命令面板插件 # Translator for Command Palette winget install 9NSHZ9B3KJFW scoop部分 建议全局安装的会带sudo和-g\n如果是win10的话就先安装一个sudo\nwin11的话设置 \u0026gt;\u0026gt; 开发者选项里面启用sudo\nscoop install sudo 添加scoop存储库 scoop bucket add extras scoop bucket add nerd-fonts scoop bucket add version 实用工具 # 多线程下载器，scoop也可以调用 sudo scoop install aria2 -g # 提取安装包文件 sudo scoop install uniextract2 -g # hex编辑软件，界面清爽 scoop install imhex # 经典游戏作弊器，有时候用来改一些软件的数值 scoop install cheat-engine # 写这篇博客就在用的markdown编辑器 scoop install obsidian # 硬件信息与温度检测 scoop install hwinfo # 简易文件服务器，快速传文件 scoop install miniserve 清理用的小工具 # 文件占用空间可视化，方便快速清理大文件 scoop install wiztree # 用来卸载有安装包的软件，这个卸载还带清理注册表 scoop install geekuninstaller # 清理多余的安装包残留 scoop install patchcleaner Coding sudo scoop install clangd -g sudo scoop install cmake -g sudo scoop install xmake -g scoop install miktex # 开源的git图形化管理工具 scoop install sourcegit # 好用的开源AI Coding工具 sudo scoop install opencode -g 生 产 力 # 开源轻量的office套件，足以胜任大部分的情况 scoop install onlyoffice-desktopeditors # KDE下面的开源的绘画软件，可以用来做图像编辑 scoop install krita # 开源的面向工业的3DCAD软件 scoop install freecad # 编写曲谱的软件 scoop install musescore # Tracker编曲软件 scoop install furnace 字体 # 综合用下来适配度最高的字体 sudo scoop install DejaVuSansMono-NF -g 其他 小米互联服务\n微软商店安装命令\nsudo wsreset -i ","date":"2024-10-27T02:29:35+08:00","permalink":"/post/categories/backup/%E9%85%8D%E7%BD%AEwindows/","title":"配置Windows"},{"content":"神秘实验要用python，然后运行报找不到init.tcl的问题\n理论上tcl/tk是python内置的东西，不可能找不到\n但确实是发生了这样的事情\n我的python是scoop安装的，可能跟这个有关系\n实际上，scoop安装的python是有这个东西的\n在[scoop目录]/apps/python/current/tcl里面\n并且执行python -m tkinter不会报错\n但貌似有些库搜索tcl不是在这个目录里面\n所以我们添加两个软连接就可以了\n# 自己替换下scoop目录 New-Item [scoop目录]\\apps\\python\\current\\Lib\\tcl8.6 -ItemType Junction -Target [scoop目录]\\apps\\python\\current\\tcl\\tcl8.6 New-Item [scoop目录]\\apps\\python\\current\\Lib\\tk8.6 -ItemType Junction -Target [scoop目录]\\apps\\python\\current\\tcl\\tk8.6 ","date":"2024-10-27T01:56:36+08:00","permalink":"/post/articles/2024/scoop%E5%AE%89%E8%A3%85%E7%9A%84python%E6%89%BE%E4%B8%8D%E5%88%B0tcl/","title":"scoop安装的python找不到tcl"},{"content":"\u0026lt;0x00\u0026gt; 前言 WPF开发中，总是会有些创建自定义控件的需求\n毕竟原生的控件总是不够用的\n\u0026lt;0x01\u0026gt; 原理 WPF使用xaml作为设计语言\n而xaml负责的可以简单理解成将控件实例化到对应的布局上，然后设定控件的一些属性\n既然是实例化，那么本质上就是创建了一个新的对象\n\u0026lt;!--随便定义一个按钮--\u0026gt; \u0026lt;button Content=\u0026#34;123\u0026#34;/\u0026gt; 其实在编译器看来，就相当于这样\nButton b = new(); 所以我们自定义控件，首先需要有一个类定义控件的属性，然后用xaml去描述控件行为\n那么怎么用xaml描述控件行为呢\n在修改控件的style的时候，我们可以把自己定义的style写到一个ResourceDictionary中\n然后通过合并ResourceDictionary来将自己style应用到不同的地方\nxaml描述控件行为其实也就是写一个style，所以可以采用一样的思路\n\u0026lt;0x02\u0026gt; 示例：带标签的文本输入框 这个感觉还是很常见的需求，有些时候需要文本输入框前面带一个标签，表示输入的是什么\n也就是说，我们要做的控件跟下面xaml代码效果是等价的\n\u0026lt;Grid Margin=\u0026#34;0,4\u0026#34;\u0026gt; \u0026lt;Grid.ColumnDefinitions\u0026gt; \u0026lt;ColumnDefinition /\u0026gt; \u0026lt;ColumnDefinition /\u0026gt; \u0026lt;/Grid.ColumnDefinitions\u0026gt; \u0026lt;TextBlock Grid.Column=\u0026#34;0\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34; /\u0026gt; \u0026lt;TextBox Grid.Column=\u0026#34;1\u0026#34; /\u0026gt; \u0026lt;/Grid\u0026gt; 定义控件 首先需要定义一个类，表示我们的类与其他类的不同\n// Controls/TextBoxWithLable/TextBoxWithLable.cs using System.Windows.Controls; namespace XamlTest.Controls; public class TextBoxWithLable : Control { // 后面还有别的代码，但这里先不需要 } 这样就可以了\n描述控件 然后创建一个资源字典，用来描述我们的控件\n\u0026lt;!-- Controls/TextBoxWithLable/TextBoxWithLable.xaml --\u0026gt; \u0026lt;ResourceDictionary xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:controls=\u0026#34;clr-namespace:XamlTest.Controls\u0026#34;\u0026gt; \u0026lt;Style x:Key=\u0026#34;Default_TextBoxWithLableStyle\u0026#34; TargetType=\u0026#34;{x:Type controls:TextBoxWithLable}\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;Template\u0026#34;\u0026gt; \u0026lt;Setter.Value\u0026gt; \u0026lt;ControlTemplate TargetType=\u0026#34;{x:Type controls:TextBoxWithLable}\u0026#34;\u0026gt; \u0026lt;Grid Margin=\u0026#34;0,4\u0026#34;\u0026gt; \u0026lt;Grid.ColumnDefinitions\u0026gt; \u0026lt;ColumnDefinition /\u0026gt; \u0026lt;ColumnDefinition /\u0026gt; \u0026lt;/Grid.ColumnDefinitions\u0026gt; \u0026lt;TextBlock Grid.Column=\u0026#34;0\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34; /\u0026gt; \u0026lt;TextBox Grid.Column=\u0026#34;1\u0026#34; /\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Setter.Value\u0026gt; \u0026lt;/Setter\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;Style BasedOn=\u0026#34;{StaticResource Default_TextBoxWithLableStyle}\u0026#34; TargetType=\u0026#34;{x:Type controls:TextBoxWithLable}\u0026#34; /\u0026gt; \u0026lt;/ResourceDictionary\u0026gt; 就像前面说的，通过style的模板来让xaml知道需要实例化什么控件\n这里先是定义了一个Default_TextBoxWithLableStyle的style，然后再写全局应用\n这样写的话万一用户想再基于默认样式自定义的话就比较方便了\n合并资源字典 上面的写好之后，如果直接使用会发现还是显示不了的\n这是因为我们还没有合并资源字典\n这里我建议在控件的文件夹中加一个文件，其中去合并所有控件的资源字典\n然后再在app.xaml中合并这个整合的资源字典文件\n毕竟控件开发一般会开发很多，全写app.xaml会很乱\n\u0026lt;!-- Controls/ControlsDictionary.xaml --\u0026gt; \u0026lt;ResourceDictionary xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34;\u0026gt; \u0026lt;ResourceDictionary.MergedDictionaries\u0026gt; \u0026lt;ResourceDictionary Source=\u0026#34;pack://application:,,,/Controls/TextBoxWithLable/TextBoxWithLable.xaml\u0026#34; /\u0026gt; \u0026lt;/ResourceDictionary.MergedDictionaries\u0026gt; \u0026lt;/ResourceDictionary\u0026gt; 然后在app.xaml中合并\n\u0026lt;!-- app.xaml --\u0026gt; \u0026lt;Application x:Class=\u0026#34;XamlTest.App\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:controls=\u0026#34;clr-namespace:XamlTest.Controls\u0026#34; xmlns:local=\u0026#34;clr-namespace:XamlTest\u0026#34; StartupUri=\u0026#34;MainWindow.xaml\u0026#34;\u0026gt; \u0026lt;Application.Resources\u0026gt; \u0026lt;ResourceDictionary\u0026gt; \u0026lt;ResourceDictionary.MergedDictionaries\u0026gt; \u0026lt;ResourceDictionary Source=\u0026#34;pack://application:,,,/Controls/ControlsDictionary.xaml\u0026#34; /\u0026gt; \u0026lt;/ResourceDictionary.MergedDictionaries\u0026gt; \u0026lt;/ResourceDictionary\u0026gt; \u0026lt;/Application.Resources\u0026gt; \u0026lt;/Application\u0026gt; 这里的source属性出现的很像网址的东西是资源url，我暂时没研究的这么全\n为控件创建属性 现在控件有了，但是如果在控件中输入文字我们是很难获取到的\n而且我们也修改不了文本框前面的lable部分 这是因为我们还没有为控件创建属性，这时候就需要修改TextBoxWithLable.cs文件了\n我们需要为我们的控件添加依赖属性\n// Controls/TextBoxWithLable/TextBoxWithLable.cs using System.Windows; using System.Windows.Controls; namespace XamlTest.Controls; public class TextBoxWithLable : Control { #region LableText_propdp public string LableText { get { return (string)GetValue(LableTextProperty); } set { SetValue(LableTextProperty, value); } } public static readonly DependencyProperty LableTextProperty = DependencyProperty.Register(\u0026#34;LableText\u0026#34;, typeof(string), typeof(TextBoxWithLable), new PropertyMetadata(null)); #endregion LableText_propdp #region InputText_propdp public string InputText { get { return (string)GetValue(InputTextProperty); } set { SetValue(InputTextProperty, value); } } public static readonly DependencyProperty InputTextProperty = DependencyProperty.Register(\u0026#34;InputText\u0026#34;, typeof(string), typeof(TextBoxWithLable), new PropertyMetadata(null)); #endregion InputText_propdp } 这里讲一个小技巧 创建依赖属性的代码确实多，但也挺公式的\n所以vs提供了一个模板，只要输入propdp然后敲两下tab就可以插入模板\n然后逐个替换即可\n然后修改TextBoxWithLable.xaml，绑定我们的依赖属性\n\u0026lt;!-- Controls/TextBoxWithLable/TextBoxWithLable.xaml --\u0026gt; \u0026lt;ResourceDictionary xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:controls=\u0026#34;clr-namespace:XamlTest.Controls\u0026#34;\u0026gt; \u0026lt;Style x:Key=\u0026#34;Default_TextBoxWithLableStyle\u0026#34; TargetType=\u0026#34;{x:Type controls:TextBoxWithLable}\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;Template\u0026#34;\u0026gt; \u0026lt;Setter.Value\u0026gt; \u0026lt;ControlTemplate TargetType=\u0026#34;{x:Type controls:TextBoxWithLable}\u0026#34;\u0026gt; \u0026lt;Grid Margin=\u0026#34;0,4\u0026#34;\u0026gt; \u0026lt;Grid.ColumnDefinitions\u0026gt; \u0026lt;ColumnDefinition /\u0026gt; \u0026lt;ColumnDefinition /\u0026gt; \u0026lt;/Grid.ColumnDefinitions\u0026gt; \u0026lt;TextBlock Grid.Column=\u0026#34;0\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34; Text=\u0026#34;{TemplateBinding LableText}\u0026#34; /\u0026gt; \u0026lt;TextBox Grid.Column=\u0026#34;1\u0026#34; Text=\u0026#34;{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=InputText, Mode=TwoWay}\u0026#34; /\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Setter.Value\u0026gt; \u0026lt;/Setter\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;Style BasedOn=\u0026#34;{StaticResource Default_TextBoxWithLableStyle}\u0026#34; TargetType=\u0026#34;{x:Type controls:TextBoxWithLable}\u0026#34; /\u0026gt; \u0026lt;/ResourceDictionary\u0026gt; 这里需要注意的是，TemplateBinding是单向绑定\n也就是相当于只能绑定源-\u0026gt;控件，这对TextBlock是没啥问题的\n但对于TextBox，我们希望里面的内容也可以传回来，这时候就不能只靠TemplateBinding\n所以TextBox这里的绑定就写得很长，主要就是要写一个Mode\n（TemplateBinding本身就是一种简写）\n然后这个控件就创建完成了\n","date":"2024-10-25T02:17:15+08:00","permalink":"/post/categories/wpf/wpf%E4%B8%AD%E6%80%8E%E4%B9%88%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6/","title":"WPF中怎么自定义控件"},{"content":"所以，不出意外地，我还是换回Windows了\n具体为什么呢，主要还是linux对高分屏的支持不佳\n（虽然Windows也不是很好就是）\n具体来说，是对2K这个分辨率支持不是很好\n一般来说，1080P的缩放是100%\n那么4K屏是200%，2K屏是150%\nlinux在100%和200%缩放都是不错的，但偏偏在150%缩放下工作就不是很好\n为什么呢，这就要谈到linux的图形化后端的问题了\n现在linux的图形化后端主要有两个，x11和wayland\nx11老，wayland新，然后wayland为了兼容老的还有xwayland\nwayland在150%缩放下工作的很好\n所以在前面的一些文章中，我会提到启用Wayland的问题\nx11在100%和200%缩放下可以说没啥问题，但在150%缩放下，字体会很糊\n这个是xwayland很难解决的问题\n不过很多基于x11且很难改后端的软件也意识到了这个问题，于是会在软件内内置缩放\n这样的话150%缩放下显示也是正常的\n不过总有个别软件，基于x11而且也没有写缩放\n这里点名Unity\nlinux中的Unity的图形后端是x11，而且我找了半天，我也找不到哪里改UI缩放\n（真的没有UI Scaling这个选项）\n于是Unity中控件字体非常小，需要放大镜看\n这样的例子还有很多\n所以，我也理解了为啥很多用linux的根本不用图形化\nlinux的图形化有些时候是不可靠的，我总算是认识到这点\nlinux，还是命令行来得靠谱\n我也想不到linux中使用Windows的最佳实践\nWindows下倒是有使用linux的最佳实际，也就是WSL\n而且像Unity这样的软件我还是要用的，所以我还是换回了Windows了\n顺便说下win11LTSC的体验吧\n我觉得还是很舒服的，LTSC非常干净，基本没啥怪东西\n而且不像win10LTSC，没有删除vclib140这种依赖，都不用自己再装\n总的来说只有Windows传奇兼容性导致四世同堂的UI问题了\n要是Windows能彻底重构一下就好了\n","date":"2024-10-25T02:16:14+08:00","permalink":"/post/articles/2024/%E6%8D%A2%E5%9B%9Ewin11%E4%BA%86/","title":"换回Win11了"},{"content":"这篇文章主要是记录命令的，所以没啥原理讲解\n\u0026lt;0x01\u0026gt; 安装zsh与插件 # Debian系 sudo apt install zsh sudo apt install zsh-autosuggestions zsh-syntax-highlighting # Arch系 sudo pacman -S zsh sudo pacman -S zsh-autosuggestions zsh-syntax-highlighting \u0026lt;0x02\u0026gt; 设置为默认shell chsh -s /usr/bin/zsh 重新shell进入zsh的初始配置界面\n\u0026lt;0x03\u0026gt; 配置主题 通过git下载 找个地方放这个主题\ngit clone --depth=1 https://github.com/romkatv/powerlevel10k.git # github访问不上的话 git clone --depth=1 https://gitee.com/romkatv/powerlevel10k.git 修改zsh启动行为\nnano ~/.zshrc 然后把这些复制到最后\nsource [git下来的文件的目录]/powerlevel10k/powerlevel10k.zsh-theme 通过yay安装 yay zsh-theme-powerlevel10k 然后把这些复制到.zshrc最后\nnano ~/.zshrc source /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme \u0026lt;0x04\u0026gt; 配置插件 apt和pacman的安装路径不同，导致需要添加的文本不同 都是在.zshrc最后添加\n# Debian系 source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh # Arch系 source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh 配置完重新启动下会话，应该就生效了\n","date":"2024-10-24T08:47:43+08:00","permalink":"/post/articles/2024/%E7%AE%80%E5%8D%95%E9%85%8D%E7%BD%AEzsh/","title":"简单配置zsh"},{"content":"因为学校的操作系统课程设计要用这玩意，所以就要配置这个玩意\n配置这玩意相对还是快的，但我也走了点弯路\n如果图方便的话我还是建议用Docker方式安装\n我的环境是WSL的Debian\n我也会在github上更新我的实现代码\n之前参考的任务说明是北大的pintos文档\n但是学校给的代码是CS162最新的代码，里面代码结构变化很大\n我决定还是保留之前写的lab1和lab2，但后面就只写学校的版本了\nPKU-Docker\nBerkeley-Docker\n\u0026lt;0x00\u0026gt; 前置 需要配置WSL，然后安装Debian\n目前的Debian版本是12\n如果后面的版本遇到些gcc编译器问题的话就换老版本的Ubuntu LTS版本吧\n然后需要git和稳定的维图诺·普莱维特·奈特沃克\n\u0026lt;0x01\u0026gt; 拉取pintos仓库 git clone git://pintos-os.org/pintos-anon --depth=1 需要提前配置下git的proxy，不然可能会很慢\n如果是ZJUT学生，做课设的话老师会给一个学校自己的gitlab链接\n用那个链接克隆一个仓库即可\n（其实克隆CS162的也可以，貌似链接年年换，这里就不贴了，Github一搜就有）\n\u0026lt;0x02\u0026gt; 配置Debian需要的软件包 # 编译环境 sudo apt install make build-essential # qemu模拟器 sudo apt install qemu # 安装调试器 sudo apt install gdb 慢的话就换下apt的源\n\u0026lt;0x03\u0026gt; 简单配置 修改./threads/Make.vars中的SIMULATOR\nSIMULATOR = --qemu 然后修改下./utils/pintos\n# 104行左右 $sim = \u0026#34;qemu\u0026#34; if !defined $sim; 20年之后的linux貌似不会提供stropts.h这个头文件\n但我们直接系统里创建一个空的就可以了（注释掉也行）\nsudo nano /usr/include/stropts.h 随便输入点注释的东西然后保存就可以\n再注释./utils/squish-pty.c中下面的代码\n// 大概在288行 if (isastream (slave)) { if (ioctl (slave, I_PUSH, \u0026#34;ptem\u0026#34;) \u0026lt; 0 || ioctl (slave, I_PUSH, \u0026#34;ldterm\u0026#34;) \u0026lt; 0) fail_io (\u0026#34;ioctl\u0026#34;); } 这样就可以了\n（我不是很喜欢动path这种东西，网上说的其他修改貌似也都不是很关键）\n\u0026lt;0x04\u0026gt; 尝试运行 在工作路径./src下面一条条执行\n# 编译utils的文件 cd ./utils make # 回到./src cd .. # 编译内核 cd ./threads make # 进入编译好的内核文件夹 cd ./build # 通过提供的脚本运行pintos ../../utils/pintos -- run alarm-multiple 理论上pintos就会运行了\n\u0026lt;0x05\u0026gt; 在vscode中调试 这部分参考了这个仓库的配置文件\n原本这个东西教程是在命令行用gdb调试\n能调试但不方便\n而vscode的C/C++插件的调试功能就是靠gdb实现的\n我们能不能让vscode调用gdb连接到pintos进行调试呢？\n答案是可以的，只是需要亿点配置\nvscode连接wsl 这个在用vscode连接wsl的时候会跳提示，要求安装wsl的插件\n安装就可以了\n安装完会多很多插件，这是对的\n点击左下角的图标，选择connect to WSL\n然后vscode就会自己配置远程环境，只要WSL网络正常\n安装C/C++插件 跟平时不同的是，我们需要给WSL安装C/C++插件，不过这很简单\n前面跟往常一样，先是搜索C++，然后安装插件\n不同的是安装后，会提示要不要在远程WSL那边安装，安装就可以了\n然后vscode就会自己装了，等就可以了\n（vscode全自动）\ntasks.json和launch.json 这俩是什么 如果是经常用vscode写C++的用户，多少对这玩意有点印象\n这两个东西算是调试控制脚本和编译控制脚本\ntasks控制怎么启动应用，launch控制怎么启动调试程序\n这两个东西一般是在./.vscode文件夹里面的\n直接创建就好，vscode会自己识别的\n如果理解的话可以跳过这部分直接从附录找写好的文件\n配置tasks.json 创建tasks.json，然后输入这些东西\n{ \u0026#34;version\u0026#34;: \u0026#34;2.0.0\u0026#34;, \u0026#34;tasks\u0026#34;: [ { \u0026#34;label\u0026#34;: \u0026#34;[Lab1] compile\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;shell\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;make\u0026#34;, \u0026#34;options\u0026#34;: { \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}/threads\u0026#34; } }, { \u0026#34;label\u0026#34;: \u0026#34;[Lab1] Run In GDB Mode\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;shell\u0026#34;, \u0026#34;isBackground\u0026#34;: true, \u0026#34;problemMatcher\u0026#34;: [ { \u0026#34;pattern\u0026#34;: [ { \u0026#34;regexp\u0026#34;: \u0026#34;.\u0026#34;, \u0026#34;file\u0026#34;: 1, \u0026#34;location\u0026#34;: 2, \u0026#34;message\u0026#34;: 3 } ], \u0026#34;background\u0026#34;: { \u0026#34;activeOnStart\u0026#34;: true, \u0026#34;beginsPattern\u0026#34;: \u0026#34;.\u0026#34;, \u0026#34;endsPattern\u0026#34;: \u0026#34;.\u0026#34;, } } ], \u0026#34;dependsOn\u0026#34;: [ \u0026#34;[P1] compile\u0026#34; ], \u0026#34;command\u0026#34;: \u0026#34;../../utils/pintos -v -k --gdb -- run ${input:test_name}\u0026#34;, \u0026#34;options\u0026#34;: { \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}/threads/build\u0026#34; } } ], \u0026#34;inputs\u0026#34;: [ { \u0026#34;id\u0026#34;: \u0026#34;test_name\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;promptString\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Name of the Test to Debug\u0026#34; } ] } 这里创建了两个task，一个负责编译，一个复制运行\n如果是自己配置的话注意路径\n配置launch.json 创建launch.json，输入下面的东西\n{ \u0026#34;version\u0026#34;: \u0026#34;0.2.0\u0026#34;, \u0026#34;configurations\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;[Lab1] Debug\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;cppdbg\u0026#34;, \u0026#34;request\u0026#34;: \u0026#34;launch\u0026#34;, \u0026#34;program\u0026#34;: \u0026#34;${workspaceFolder}/threads/build/kernel.o\u0026#34;, \u0026#34;miDebuggerServerAddress\u0026#34;: \u0026#34;127.0.0.1:1234\u0026#34;, \u0026#34;preLaunchTask\u0026#34;: \u0026#34;[P1] Run Test in GDB mode\u0026#34;, \u0026#34;stopAtEntry\u0026#34;: false, \u0026#34;cwd\u0026#34;: \u0026#34;${workspaceFolder}\u0026#34;, \u0026#34;environment\u0026#34;: [], \u0026#34;externalConsole\u0026#34;: false, \u0026#34;MIMode\u0026#34;: \u0026#34;gdb\u0026#34;, \u0026#34;setupCommands\u0026#34;: [ { \u0026#34;text\u0026#34;: \u0026#34;-enable-pretty-printing\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Enable pretty-printing for gdb\u0026#34;, \u0026#34;ignoreFailures\u0026#34;: true }, { \u0026#34;text\u0026#34;: \u0026#34;source -v ${workspaceFolder}/misc/gdb-macros\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Import Pintos GDB macros file\u0026#34;, \u0026#34;ignoreFailures\u0026#34;: false } ], \u0026#34;symbolLoadInfo\u0026#34;: { \u0026#34;loadAll\u0026#34;: true, \u0026#34;exceptionList\u0026#34;: \u0026#34;\u0026#34; } } ] } 记得注意路径\n尝试运行 首先在./thread/init.c里面找到main，打个断点，然后运行\n理论上这时候运行会让你选配置文件，选我们创建的debug配置\n这时候就像运行别的C++项目一样，开始debug，体验好多了\n需要注意的是，上面的配置仅针对lab1，后面的可能需要单独配置\n（后面的我还没开始写）\n\u0026lt;0x06\u0026gt; 配置Test 因为没把pintos配置到path中，所以在./thread/build下执行make test会找不到pintos\n有把pintos添加到path中就不用动这些东西\n这时候就要修改./test下面的Make.tests文件\n# 大概在55行 TESTCMD = ../../utils/pintos -v -k -T $(TIMEOUT) TESTCMD += $(SIMULATOR) TESTCMD += $(PINTOSOPTS) 这样就可以了\n警钟长鸣：不要用Arch编译 如果使用Arch编译这个东西的话可能会遇到loader.bin的大小有128M\n而不是正常的512字节\n不只是WSL中Arch会出现这个问题，物理机Manjaro也会这样\n具体原因不是很清楚，我怀疑是gcc版本问题\n我测试过Ubuntu22的gcc11和Debian12的gcc12，这两个都可以成功编译\n但Arch的是gcc14，可能较高的gcc版本导致编译行为不符合预期\n不过我暂时还没尝试在Arch中使用gcc11编译过 （源码编译gcc时间太长了，就摸了）\n","date":"2024-10-24T00:40:35+08:00","permalink":"/post/categories/cs162/%E4%BC%98%E9%9B%85%E9%85%8D%E7%BD%AEpintos/","title":"优雅配置pintos"},{"content":"xmake对于个人项目来说，构建比cmake简单太多，少折腾太多\n但在我之前测试的时候，发现xmake链接glfw库的时候会出链接错误\n这个问题是xmake在获取包时，倾向使用静态的库\n这就导致部分包在链接时会无法解析符号之类的问题\n（有些包反过来，使用静态库会无法解析符号，这次的例子也有）\n至于为什么，我目前也不是很清楚\n总之，打开xmake.lua\nadd_rules(\u0026#34;mode.debug\u0026#34;, \u0026#34;mode.release\u0026#34;) add_requires(\u0026#34;glad\u0026#34;) add_requires(\u0026#34;glfw3\u0026#34;, {configs = {shared = true}}) target(\u0026#34;XmakeTest\u0026#34;) set_kind(\u0026#34;binary\u0026#34;) set_languages(\u0026#34;cxx17\u0026#34;) set_warnings(\u0026#34;all\u0026#34;) add_packages(\u0026#34;glad\u0026#34;) add_packages(\u0026#34;glfw3\u0026#34;) add_files(\u0026#34;src/*.cpp\u0026#34;) set_toolchains(\u0026#34;clang\u0026#34;) 别的不重要，关键是这两句add_requires()\n对于glad，xmake默认会使用静态库，并且经过测试，使用动态库会报错\n对于glfw3，通过{configs = {shared = true}}，指定使用动态库\n这样之后，编译就不会找不到符号了\n下面做一些进阶一些的开发，经典做imgui的开发\n测试代码在之前这篇博客有写\n不过xmake.lua还是有点坑的，这里贴一下\nadd_rules(\u0026#34;mode.debug\u0026#34;, \u0026#34;mode.release\u0026#34;) add_requires(\u0026#34;fmt\u0026#34;) add_requires(\u0026#34;glad\u0026#34;) add_requires(\u0026#34;glfw3\u0026#34;, {configs = {shared = true}}) add_requires(\u0026#34;vcpkg::imgui\u0026#34;,{ configs = { features = { \u0026#34;opengl3-binding\u0026#34;, \u0026#34;glfw-binding\u0026#34;, \u0026#34;docking-experimental\u0026#34; }, shared = true } }) target(\u0026#34;XmakeTest\u0026#34;) set_kind(\u0026#34;binary\u0026#34;) set_languages(\u0026#34;cxx17\u0026#34;) set_warnings(\u0026#34;all\u0026#34;) add_packages(\u0026#34;fmt\u0026#34;) add_packages(\u0026#34;glad\u0026#34;) add_packages(\u0026#34;glfw3\u0026#34;) add_packages(\u0026#34;vcpkg::imgui\u0026#34;) add_files(\u0026#34;src/*.cpp\u0026#34;) set_toolchains(\u0026#34;clang\u0026#34;) 使用vcpkg来获取imgui库，然后通过清单指定特性，还要指定动态库\n一切正常的话结果应该是这样的\n","date":"2024-08-31T21:31:55+08:00","permalink":"/post/articles/2024/xmake%E5%81%9Aopengl%E5%BC%80%E5%8F%91%E6%97%B6%E6%97%A0%E6%B3%95%E8%A7%A3%E6%9E%90%E7%AC%A6%E5%8F%B7/","title":"xmake做opengl开发时无法解析符号"},{"content":"考虑到C++主流还是用CMake做构建，最近也稍微折腾了下CMake\n然后C++的话还是要用些包管理的，不然手动管理太抽象了\n所以还需要配置下vcpkg的东西\n虽然微软这确实有文档讲怎么配，不过讲得不是很清楚\n\u0026lt;0x00\u0026gt; 前置准备 系统方面 CMake 既然是CMake配置，所以系统里肯定要安装CMake的\n命令行敲cmake能找到就行，我这里采用scoop安装\nscoop install cmake vcpkg 然后是包管理器vcpkg\n网上很多教程是下载源码的，我这里也是采用scoop安装\nscoop install vcpkg 采用scoop安装的话会自动添加VCPKG_ROOT的环境变量，指向的就是安装路径\n虽然这个主要是方便MSBuild找vcpkg的，跟这篇博客没关系\n（自己下载源码的话手动添加一个就可以了）\nVSCode方面 主要是C/C++ Extension Pack和CMake Tools\n\u0026lt;0x01\u0026gt; CMake工程配置 首先新建一个文件夹，相当于是工程文件夹了\n快速创建CMake工程 VSCode中Ctrl+Shift+P调出命令面板\n输入CMake: Quick Start后回车\n在输入完名字之类的东西之后，会让你设置一个preset\n这个比较关键，选择最后从编译器创建\n（windows平台下貌似直接绑定MSVC，选别的编译器也会换成MSVC）\n这时候会创建一些文件，先不管\n初始化vcpkg 打开终端窗口，输入下面的命令\nvcpkg new --application 这时候也会创建一些文件，也不需要管\n让CMake认识vcpkg 打开CMakePresets.json，里面可以看到一些配置\n{ \u0026#34;version\u0026#34;: 8, \u0026#34;configurePresets\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;clang\u0026#34;, \u0026#34;displayName\u0026#34;: \u0026#34;Clang 18.1.8 x86_64-pc-windows-msvc\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;Using compilers: C = C:\\\\Users\\\\cookie\\\\scoop\\\\apps\\\\llvm\\\\current\\\\bin\\\\clang.exe, CXX = C:\\\\Users\\\\cookie\\\\scoop\\\\apps\\\\llvm\\\\current\\\\bin\\\\clang++.exe\u0026#34;, \u0026#34;binaryDir\u0026#34;: \u0026#34;${sourceDir}/out/build/${presetName}\u0026#34;, \u0026#34;cacheVariables\u0026#34;: { \u0026#34;CMAKE_INSTALL_PREFIX\u0026#34;: \u0026#34;${sourceDir}/out/install/${presetName}\u0026#34;, \u0026#34;CMAKE_C_COMPILER\u0026#34;: \u0026#34;C:/Users/cookie/scoop/apps/llvm/current/bin/clang.exe\u0026#34;, \u0026#34;CMAKE_CXX_COMPILER\u0026#34;: \u0026#34;C:/Users/cookie/scoop/apps/llvm/current/bin/clang++.exe\u0026#34;, \u0026#34;CMAKE_BUILD_TYPE\u0026#34;: \u0026#34;Debug\u0026#34; } } ] } （这里贴出我的结果，指定为clang）\n这时候，CMake其实不知道vcpkg在哪里，我们需要让它知道vcpkg在哪\n我先贴出最后的结果\n{ \u0026#34;version\u0026#34;: 8, \u0026#34;configurePresets\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;default\u0026#34;, \u0026#34;binaryDir\u0026#34;: \u0026#34;${sourceDir}/out/build\u0026#34;, // 添加的环境变量 \u0026#34;environment\u0026#34;: { // 这里写上vcpkg的安装路径 \u0026#34;VCPKG_ROOT\u0026#34;: \u0026#34;C:\\\\Users\\\\cookie\\\\scoop\\\\apps\\\\vcpkg\\\\current\u0026#34;, // 魔法上网的话 \u0026#34;HTTP_PROXY\u0026#34;: \u0026#34;127.0.0.1:1000\u0026#34;, \u0026#34;HTTPS_PROXY\u0026#34;: \u0026#34;127.0.0.1:1000\u0026#34; }, \u0026#34;cacheVariables\u0026#34;: { // 让CMake可以找到vcpkg，一般这样写就可以 \u0026#34;CMAKE_TOOLCHAIN_FILE\u0026#34;: \u0026#34;$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake\u0026#34;, \u0026#34;CMAKE_INSTALL_PREFIX\u0026#34;: \u0026#34;${sourceDir}/out/install/${presetName}\u0026#34;, // 因为windows下vcpkg自己会覆盖编译器设置为MSVC，所以也可以不指定 \u0026#34;CMAKE_BUILD_TYPE\u0026#34;: \u0026#34;Debug\u0026#34; } } ] } 已经通过注释指出需要添加的部分，写完就好了，顺便删点不需要的\n这里相当于是一个虚拟环境，如果有其他要用到的环境变量的话也要写进environment里\n\u0026lt;0x02\u0026gt; 实战测试 这里做一个简单的opengl开发演示\nvcpkg中添加需要的包 打开终端，指定vcpkg需要glfw3和glad这两个包\nvcpkg add port glfw3\rvcpkg add port glad CMake中引用并链接包 然后，打开CMakeLists.txt\ncmake_minimum_required(VERSION 3.10) project(CmakeTest) add_executable(CmakeTest main.cpp) # 引用包 find_package(glad CONFIG REQUIRED) find_package(glfw3 CONFIG REQUIRED) # 链接包 target_link_libraries(CmakeTest PRIVATE glfw) target_link_libraries(CmakeTest PRIVATE glad::glad) 在代码中使用 最后，打开main.cpp\n#include \u0026lt;glad/glad.h\u0026gt; #include \u0026lt;GLFW/glfw3.h\u0026gt; #include \u0026lt;iostream\u0026gt; void framebuffer_size_callback(GLFWwindow* window, int width, int height); void processInput(GLFWwindow *window); const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; int main() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, \u0026#34;LearnOpenGL\u0026#34;, NULL, NULL); if (window == NULL) { std::cout \u0026lt;\u0026lt; \u0026#34;Failed to create GLFW window\u0026#34; \u0026lt;\u0026lt; std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout \u0026lt;\u0026lt; \u0026#34;Failed to initialize GLAD\u0026#34; \u0026lt;\u0026lt; std::endl; return -1; } while (!glfwWindowShouldClose(window)) { processInput(window); glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); glfwSwapBuffers(window); glfwPollEvents(); } glfwTerminate(); return 0; } void processInput(GLFWwindow *window) { if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } （这段代码是Learn OpenGL的代码，直接复制就能跑，所以就删注释了）\n编译成功并运行的话应该是这样的\n一切正常的话，恭喜完成配置，之后这样配置也能用\n\u0026lt;0x03\u0026gt; 进阶设置 如果使用的包有可选特性 在vcpkg中，有一些包是有可选特性的\n比如说imgui这个库，有很多可选特性，比如docking，opengl3，win32等等\n比方说做opengl的开发，添加包的时候可以这么写\nvcpkg add port imgui[opengl3-binding,glfw-binding,docking-experimental] 然后修改CMakeLists.txt部分，加上下面的东西\nfind_package(imgui CONFIG REQUIRED) # example改成自己的项目 target_link_libraries(example PRIVATE imgui::imgui) 然后打开main.cpp测试下\n#include \u0026#34;imgui.h\u0026#34; #include \u0026#34;imgui_impl_glfw.h\u0026#34; #include \u0026#34;imgui_impl_opengl3.h\u0026#34; #include \u0026lt;fmt/core.h\u0026gt; #include \u0026lt;glad/glad.h\u0026gt; #include \u0026lt;GLFW/glfw3.h\u0026gt; // glfw错误回调 static void glfw_error_callback(int error, const char *description) { fmt::println(\u0026#34;GLFW Error {}: {}\u0026#34;, error, description); } int main(int, char **) { glfwSetErrorCallback(glfw_error_callback); if (!glfwInit()) { return -1; } // 设定opengl版本 const char *glsl_version = \u0026#34;#version 130\u0026#34;; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); // 创建窗口 GLFWwindow *window = glfwCreateWindow(1280, 720, \u0026#34;ImGui GLFW+OpenGL3 with docking example\u0026#34;, nullptr, nullptr); if (window == nullptr) { return -1; } glfwMakeContextCurrent(window); glfwSwapInterval(1); // 初始化glad if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { fmt::println(\u0026#34;Failed to initialize GLAD\u0026#34;); return -1; } // 初始化imgui IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO \u0026amp;io = ImGui::GetIO(); (void)io; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // 启用docking io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; ImGui::StyleColorsDark(); // 启用docking的后处理 ImGuiStyle\u0026amp; style = ImGui::GetStyle(); if (io.ConfigFlags \u0026amp; ImGuiConfigFlags_ViewportsEnable) { style.WindowRounding = 0.0f; style.Colors[ImGuiCol_WindowBg].w = 1.0f; } ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init(glsl_version); // 渲染循环 while (!glfwWindowShouldClose(window)) { // 初始化渲染 glfwPollEvents(); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); // 渲染demo窗口 ImGui::ShowDemoWindow(); // 真正开始渲染 ImGui::Render(); int display_w, display_h; glfwGetFramebufferSize(window, \u0026amp;display_w, \u0026amp;display_h); glViewport(0, 0, display_w, display_h); glClearColor(.1f, .1f, .1f, 1); glClear(GL_COLOR_BUFFER_BIT); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); // docking处理 if (io.ConfigFlags \u0026amp; ImGuiConfigFlags_ViewportsEnable) { GLFWwindow* backup_current_context = glfwGetCurrentContext(); ImGui::UpdatePlatformWindows(); ImGui::RenderPlatformWindowsDefault(); glfwMakeContextCurrent(backup_current_context); } glfwSwapBuffers(window); } // 退出的处理 ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); return 0; } （稍微修改了imgui给的示例，使用了glad和fmt）\n对于多人开发的情况 前面提到，我们可以用CMakePresets.json来统一设定CMake的变量\n但对于多人开发的情况，这个方式有一些弊端\n比如，windows下，不同人编译器的位置就可能不一样\n这时候，可以引入CMakeUserPresets.json文件做具体的设定\n比方说接着上面的配置，我们可以把CMakePresets.json改造成这样\n{ \u0026#34;version\u0026#34;: 8, \u0026#34;configurePresets\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;default\u0026#34;, \u0026#34;binaryDir\u0026#34;: \u0026#34;${sourceDir}/out/build\u0026#34;, \u0026#34;cacheVariables\u0026#34;: { \u0026#34;CMAKE_TOOLCHAIN_FILE\u0026#34;: \u0026#34;$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake\u0026#34;, \u0026#34;CMAKE_INSTALL_PREFIX\u0026#34;: \u0026#34;${sourceDir}/out/install/${presetName}\u0026#34;, \u0026#34;CMAKE_BUILD_TYPE\u0026#34;: \u0026#34;Debug\u0026#34; } } ] } 加上CMakeUserPresets.json，内容为\n{ \u0026#34;version\u0026#34;: 8, \u0026#34;configurePresets\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;My default\u0026#34;, \u0026#34;inherits\u0026#34;: \u0026#34;default\u0026#34;, \u0026#34;environment\u0026#34;: { \u0026#34;SCOOP_ROOT\u0026#34;: \u0026#34;C:/Users/cookie/scoop\u0026#34;, \u0026#34;VCPKG_ROOT\u0026#34;: \u0026#34;$env{SCOOP_ROOT}/apps/vcpkg/current\u0026#34;, \u0026#34;HTTP_PROXY\u0026#34;: \u0026#34;127.0.0.1:1000\u0026#34;, \u0026#34;HTTPS_PROXY\u0026#34;: \u0026#34;127.0.0.1:1000\u0026#34; } } ] } CMakePresets.json定义总体的框架，CMakeUserPresets.json定义具体的位置\n在git设置中，排除CMakeUserPresets.json就可以了\n","date":"2024-08-29T21:17:56+08:00","permalink":"/post/articles/2024/%E5%9C%A8vscode%E4%B8%AD%E4%BD%BF%E7%94%A8cmake+vcpkg/","title":"在vscode中使用cmake+vcpkg"},{"content":"我的vscode其中一个配置是xmake+clangd的配置\n因为要写OpenGL相关的代码，clangd中默认头文件排序就很头疼\n找了半天方法，虽然没找到直接配置clangd.arguments之类的方法\n但也是有相对简单的方法解决这个问题\n在项目根目录新建.clang-format文件\n输入下面的内容\nBasedOnStyle: Microsoft SortIncludes: Never 因为我之前就是使用clangd自带的Microsoft配置\n这里的BasedOnStyle相当于继承这个配置\n下面的一句是关闭头文件排序\n","date":"2024-08-26T16:04:41+08:00","permalink":"/post/articles/2024/%E5%85%B3%E9%97%ADclangd%E5%A4%B4%E6%96%87%E4%BB%B6%E6%8E%92%E5%BA%8F/","title":"关闭clangd头文件排序"},{"content":"暑假经典节目，与Linux大战300回合\n这次我打算好好配置下Linux并作为日常使用\n发行版的话我使用Manjaro，因为比Arch好配置，也有AUR的便捷\n笔记本是机革的无界14+\n因为是在笔记本上装，就没啥截图了\n\u0026lt;0x00\u0026gt; 安装准备 简单来说，找个U盘，用Ventoy做启动盘，然后把Manjaro的ISO放进去就可以\n选择你的桌面环境 ManjaroISO\n选哪个桌面环境呢，我最后选择了KDE的版本\n经过更新后，KDE的设计总算跟上时代了，颜值不错\n并且KDE环境可设置选项也挺多的，自带很多实用小工具\n其他的不是说不好，但确实不合适\nGnome一些习惯我适应不了，xfce不是很适合高分屏，其他的没试过\n用Ventoy做引导 Ventoy可以直接引导ISO启动，然后通过ISO本身装系统\n加上ISO格式的PE，完全可以当成PE用，并且装系统也很方便\n如果有装Scoop或Winget，直接敲命令安装\n# Scoop scoop install ventoy # Winget winget install Ventoy.Ventoy 没有的话就自己下载吧\nVentoy下载\n下载完后，找个U盘，打开Ventoy，安装即可\n安装好后，把下载好的ISO拖进U盘就可以\n\u0026lt;0x01\u0026gt; 安装Manjaro U盘插上笔记本，开机，狂按那几个进BIOS的键\n（ESC，F1，F2，F3，F10，F11，F12，DEL）\n进BIOS后，选择U盘启动\n会进入Ventoy的引导界面，选择之前导入的ISO\n然后系统会从ISO启动，进入Manjaro的启动界面\n这里它默认会使用开源驱动启动（Boot with open source drivers）\n我建议是按↓方向键，选择使用专有驱动启动（Boot with proprietary drivers）\n这样的话之后安装的也是专有驱动，遇到的驱动问题会少点\nManjaro安装环境正式启动后，你可以先试试看这个系统\n如果发现系统元素很小，而且调整系统缩放会导致比例很奇怪\n这个没什么办法，因为安装环境下使用的窗口服务是X11，对高分辨率支持不是很好\n不过安装后就可以用新的Wayland启动了，这个对高分辨率支持不错的\n确定要安装后，就可以双击安装器进行安装，这个跟着安装器指引就可以\n安装完重启，就可以进入Manjaro系统了\n\u0026lt;0x02\u0026gt; 安装完要干的事 配置速度快的源 Manjaro内置自动配置快速软件源的工具\n打开终端\nsudo pacman-mirrors -g 执行即可，就是因为要把世界上所有源都试一遍，所以等待时间会有点久\n更新所有的包 可以通过升级/删除软件包用图形化界面更新所有的包\n也可以敲命令\nsudo pacman -Syyu 顺便也可以看看前面配置的源的速度\n安装yay yay是AUR的管理器，需要注意的是，AUR并非Arch官方维护，需要自己甄别软件的安全性\n不过也正是这样，通过AUR可以非常轻松地获取其他软件\nsudo pacman -S yay base-devel 因为AUR的软件需要从源码编译，需要安装base-devel提供一些基础的工具链\nyay在安装软件时，会优先检索pacman中的内容，并且语法上跟pacman是一致的\n所以之后软件的安装/升级/删除都可以交给yay\n需要注意的是yay安装软件时不需要sudo，在需要权限提升时，它会要求输入密码的\n然后安装软件的时候，如果后缀有带bin的，一般说明是二进制版本，优先选择这个\n这样的话就不用从源代码编译了，安装会快很多\n配置fcitx5 fcitx5是一个输入法框架，装上后再配置下就可以输入中文了\nsudo pacman -S fcitx5 fcitx5-configtool fcitx5-breeze fcitx5-chinese-addons fcitx5-gtk fcitx5-qt 安装好后，在系统设置内会多一个输入法的页面，先启动下输入法\n然后在系统设置\u0026gt;\u0026gt;键盘\u0026gt;\u0026gt;虚拟键盘中选择fcitx5即可\n再在输入法\u0026gt;\u0026gt;配置附加组件中稍微配置配置些主题之类的就可以了\n为非Wayland应用配置输入法 目前的配置只能在使用Wayland的窗口中使用输入法\n而Linux中仍有许多软件是跑在X11上的\n对于这些软件我们需要通过环境变量让它们识别输入法\n我这里是把环境变量写到/etc/profile的方式实现\nManjaro的/etc/profile实际上是遍历运行/etc/profile.d文件夹的内容\n所以最好是在/etc/profile.d中新建文件，在里面写上环境变量\ncd /etc/profile.d nano set-fcitx5.sh 在文件中输入\nexport XMODIFIERS=@im=fcitx 保存退出后，重启系统，这块就配置好了\n现在对于大部分运行在x11中的应用是处理完了\n但对于部分软件，可能仍无法使用输入法，这就需要对软件单独配置环境变量\n# 如果软件使用QT框架 QT_IM_MODULE=fcitx # 如果软件使用GTK框架 GTK_IM_MODULE=fcitx 这两句虽然是可以直接塞在前面的配置文件中当全局变量，但官方不推荐\n\u0026lt;0x03\u0026gt; 安装其他的软件 VS Code 经常用的开发环境\nyay visual-studio-code-bin 安装完修改启动参数来启用wayland\n--enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime Edge 目前我的大部分浏览器数据在Edge上\nyay microsoft-edge-stable-bin 安装完修改启动参数来启用wayland，参数跟上面的VS Code一样\nhiddify 魔法学，不细讲\nyay hiddify-next-bin \u0026lt;0x04\u0026gt; 其他实用命令 删除不需要的依赖 yay -Rns $(yay -Qdt) 查看窗口运行服务 对于高分屏，wayland的支持是好得多的\n即使KDE跑x11应用时，由于允许应用自行缩放，所以大部分情况下还是清晰的\n但跑在wayland下可以杜绝大部分控件缩放问题\n# qdbus可能不存在，改为qdbus6 qdbus org.kde.KWin /KWin org.kde.KWin.showDebugConsole 运行后会跳出一个小工具，用来查看所有窗口运行在哪个服务上\n\u0026lt;0x05\u0026gt; 遇到的奇怪闪屏问题 不知道为什么，我安装完系统后发现系统总会有奇怪的闪屏\n换了其他桌面系统后多少都会有点\n在折腾了很多包之后，我大概定位到是AMD的2D驱动问题\nAMD的显卡驱动很散，OpenGL驱动和Vulkan驱动都是分开装的\nManjaro默认不会装Xorg的2D加速驱动，这可能是导致奇怪闪屏的原因\n安装的话很简单\nyay -S xf86-video-amdgpu 安装上之后，闪屏遇到的少了，但不能根除\n不排除Vulkan驱动的问题，AMDGPU的节能选项的问题\n","date":"2024-07-28T12:47:37+08:00","permalink":"/post/articles/2024/%E6%9C%BA%E9%9D%A9%E6%9C%AC%E6%9C%AC%E8%A3%85linux/","title":"机革本本装Linux"},{"content":"数字图像处理我们主要是讲OpenCV的（虽然实际上代码讲的不多）\n然后作为期末的课程设计，要求我们写一个OpenCV的程序\n主要就是用OpenCV去解决一些现实中的问题，然后要带图形界面\n允许组队，而且在学期初就要定课设的题目\n\u0026lt;0x00\u0026gt; 我们做了什么 本来打算做一个用OpenCV做相机反求，Godot做渲染的AR项目\n（所以前面有几篇研究怎么用C#做OpenCV开发）\n引擎这块基本都搭完完了，然后估计了下感觉写不完了\n（C#写OpenCV的代码参考太少了，很多代码都是试出来的，开发效率太低了）\n好在我们还有一个Plan B，就是参考MIT的运动放大的论文去写一个应用\n（主要是看了Steve Mould的视频）\n这样的话，语言也干脆换成了参考够多的Python，效率高了不少\n问题也有，就是我们Python都不熟，但比起没代码参考好太多了\n项目地址\n\u0026lt;0x0\u0026gt; 整体架构 我们采用dearpygui来构建整个图形界面\n使用numpy和scipy来做一些数据的处理\n\u0026lt;0x0\u0026gt; EVM原理 具体看MIT EVM的介绍，这里就只做大致的讲解\n\u0026lt;0x0\u0026gt; 一些感想 关于知识 首先就是确实感觉知道的东西多反而是一种诅咒\n为什么这么说呢，知道的东西多了之后，遇到新的问题总是会用自己知道的知识解决\n这道是正常的，毕竟这也是学习的目的\n但问题是，当知道的东西多了之后，我总是觉得这个问题用已知知识是能解决的\n然后就这样去开发，走一半，会发现这个项目按原计划开发会遇到非常多的问题\n两个选择，要么花很多时间去解决，要么项目推倒重来重新设计架构\n这两个选择都是痛苦的\n像这个课设，我就犯这样的错误\n一开始我是打算用C#做Opencv开发，然后WPF做前端\n因为Opencv在C#的实现我还是熟的，WPF我也是熟的\n开发到后来发现，EVM需要用到傅里叶变换，Opencv中肯定是不带这种东西的\n这时候其实可以考虑切换到python开发，但我并没有这么做，我选择来了花时间解决\n然而，在我头铁写完了傅里叶变换的流程后，我发现整个算法的运行效率低得很\n（中间的类型转换太多了，我对Mat类型了解也不够）\n这时候又遇到一个抉择，推到重来换python还是花时间研究C#操作Mat类型\n好在这次我选择了推到重来换python，最后完成开发\n这个问题其实说到底，还是知道的不够多，毕竟这种开发中的问题懂的多了之后，是可以在开发前预知到的\n但还是有一个矛盾，对一块知识，究竟了解到什么程度才能算懂得够多，我不知道\n学的东西多了，确实是能比别人看到更远的东西，但也比别人看到更少的东西\n如果我是新手，这个项目我肯定就用python开发了，哪还要折腾C#啊\n就是因为我了解C#，我才一开始用C#开发\n关于团队 这个课设虽然可以组队，但这个项目基本是我一个人完成的\n那我的队友在干什么呢？他们在做别的可以组队的项目，并且是一人一个项目\n我发现，对于计算机的学生来说，可能最好的合作方式就是一人一个项目\n程序开发不是人越多越快的\n如果说有n个人，那么单位时间内代码量是$O(n)$，但沟通成本是$O(n^2)$\n因为程序员开发软件需要的是连贯的思路，脑中需要保存所有代码的逻辑结构\n对于别人写的代码，自己看是很花时间的，只有说问原来的开发者代码功能才是最快的\n更何况程序有BUG，命名规范等等之类的增加沟通成本的问题\n这方面可以看下《人月神话》这本书\n一个人一个项目，整个项目都在一个人的掌握之中，这样的效率是更高的\n当然，这样的合作方式不是适合所有队伍的\n你需要确保你的队友没有摆子，并且队每个人都可以分到适合其水平的项目\n最重要的，足够的信任，在队友没请求帮助前就尽量不插手队友的工作\n否则还是正常合作吧，这样起码可以保证项目可以写完\n关于python 最后这个课设用python完成的\n我在这个课设之前，可以说我是看不起python的\n总觉得这个语言效率低，语法过于抽象\n虽然我看习惯了传统的C系代码，看python这种靠tab分隔层次的语言确实需要时间适应\n但挺过阵痛期后，我发现python还挺好用 整个语言的设计完全是为解决问题去的\n通过丰富的包，让很多问题几行代码就搞定了\n不过还是有一些小问题的，但这也不是python的问题，应该说是弱类型语言的问题\n因为弱类型，导致很多问题不能在编译期查出\n虽然是可以给变量打注解，但问题也是编译期不检查这个\n所有实际开发中经常出现传参类型出问题，排查好一会儿\n总之，python这门语言感觉就像是共享单车，解决的是程序最后怎么运行的问题\n当脚本语言是非常合适的\n所以说，没有最好的语言，只有最适合的语言\n","date":"2024-07-23T12:23:45+08:00","permalink":"/post/articles/2024/%E6%B5%99%E5%B7%A5%E5%A4%A7%E6%95%B0%E5%AD%97%E5%9B%BE%E5%83%8F%E5%A4%84%E7%90%86%E8%AF%BE%E8%AE%BE%E8%AE%B0%E5%BD%95/","title":"浙工大数字图像处理课设记录"},{"content":"今天有人找我解决一些软件相关的问题，然后发现会牵扯到一些Win32的API\n一般调用Win32API都是用C++方便的，而且微软给的文档也是C++的\n我虽然对C++不是很熟，但对C#熟啊\n在C#中，我们可以通过P/Invoke去做动态库的互操作\n但对于Win32API，如果自己写P/Invoke，里面就有很多不确定的东西\n光是那个DllImport特性里面的参数就不是很好填\n那C#开发者是不是很难做Win32开发呢，那也不是\n在Nuget上有很多别的大佬包装好的Win32库，安装完可以直接调用\n但Win32中有超级多的API，这会导致代码提示里面会多出非常多东西，确实是有点干扰了\n但好在微软也提供了一个解决方案，CsWin32\n这个是基于C#源生成器的Win32API封装器\n就是需要什么API，它就去生成那些API的P/Invoke代码\n并且生成的质量还挺高的，保留原本的调用风格的同时，尽可能去迎合C#的开发体验\n\u0026lt;0x00\u0026gt; 安装CsWin32 因为会用到C#的源生成器，所以最好使用VS2022并且相对新的版本，不然代码提示之类的会出问题\nVSCode的源生成器体验也是不错的，这个直接装C# kit插件就可以了\n直接在Nuget中搜索CsWin32，包名是Microsoft.Windows.CsWin32\n这个包最低支持到.net framework 4.5也即.NET Standard 2.0\n对于这个版本，还需要安装System.Memory\n如果是.NET Standard 2.1及以上，也即.net 5.0及以上的版本，那么就不需要安装\n（如果是新项目的话应该会用新版本的框架吧）\n\u0026lt;0x01\u0026gt; 如何使用 添加需要的API 首先是要告诉源生成器要生成哪些API的封装\n在项目根目录新建叫NativeMethods.txt的文件\n在里面可以添加你需要添加的Win32函数或者结构的名字\nPssCaptureSnapshot\rPssQuerySnapshot\rPSS_PROCESS_INFORMATION\rPSS_HANDLE_INFORMATION\rPSS_VA_CLONE_INFORMATION 这里的示例是Windows进程快照相关的API，使用的API也不多，仅做演示作用\n简单解释下使用的API PssCaptureSnapshot 捕获目标进程的快照\nDWORD PssCaptureSnapshot( [in] HANDLE ProcessHandle, [in] PSS_CAPTURE_FLAGS CaptureFlags, [in, optional] DWORD ThreadContextFlags, [out] HPSS *SnapshotHandle ); ProcessHandle目标进程的句柄\nCaptureFlags指定要捕获的标志\nThreadContextFlags如果 CaptureFlags 指定线程上下文，则要捕获的 CONTEXT 记录标志\nSnapshotHandle返回此函数捕获的快照的句柄\n函数返回的是winerror.h中定义的错误代码，无错误是ERROR_SUCCESS\nPssQuerySnapshot 查询捕获的快照的信息\nDWORD PssQuerySnapshot( [in] HPSS SnapshotHandle, [in] PSS_QUERY_INFORMATION_CLASS InformationClass, [out] void *Buffer, [in] DWORD BufferLength ); SnapshotHandle要查询的快照的句柄\nInformationClass用于选择要查询的信息\nBuffer此函数提供的信息，类型由InformationClass决定\nBufferLength缓冲区的大小（以字节为单位）\n函数返回的是winerror.h中定义的错误代码，无错误是ERROR_SUCCESS\n剩下的是一些结构，东西多就不细讲了，跟本文关系不大，具体就看文档吧\n在代码中使用Win32API 这里做一个使用Win32API查询进程PID的示例\n（虽然C#的Process类对象本身就可以直接查询）\nusing System.Diagnostics; using Windows.Win32; using Windows.Win32.System.Diagnostics.ProcessSnapshotting; public class Program { public static void Main() { // 使用C#自带的Process类型获取记事本进程 Process test = Process.GetProcessesByName(\u0026#34;notepad\u0026#34;)[0]; // 声明一个查询flags PSS_CAPTURE_FLAGS flags = PSS_CAPTURE_FLAGS.PSS_CAPTURE_THREADS; // 使用Win32API来捕获进程快照 PInvoke.PssCaptureSnapshot( test.SafeHandle, flags, 0, out HPSS snapshotHandle); // 声明查询进程基本信息的变量 PSS_PROCESS_INFORMATION info; // 涉及到指针操作，所以要用unsafe块包装 unsafe { // 使用Win32API来查询进程快照信息 PInvoke.PssQuerySnapshot( snapshotHandle, PSS_QUERY_INFORMATION_CLASS.PSS_QUERY_PROCESS_INFORMATION, \u0026amp;info, (uint)sizeof(PSS_PROCESS_INFORMATION)); } Console.WriteLine(info.ProcessId); } } 确实是获取到了记事本的PID\n而且观察代码，不难发现，CsWin32会非常智能地使用C#自带的类型\n比如说Process类型里面的SafeHandle属性，这个返回的是SafeProcessHandle\n这个虽然它的命名空间是Microsoft.Win32.SafeHandles，但确实是C#本身就有的\n对于C#不带的类型，只有使用的API需要这些类型传参，CsWin32才会去生成对应的代码\n\u0026lt;0x02\u0026gt; 一些不知道类型的枚举 如果你需要使用某个枚举，但不知道是什么类型，CsWin32可以自动指出具体类型\n比如前面提到那两个API的返回值是winerror.h中定义的错误代码，但我们不知道这是什么类\n这时候可以在NativeMethods.txt里面直接加上ERROR_SUCCESS\nCsWin32会抛出警告：应该使用正确的声明\n最后也指出正确的声明是WIN32_ERROR，文件里改好就行\n这里对上面的代码稍作修改作为示例\nusing System.Diagnostics; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Diagnostics.ProcessSnapshotting; public class Program { public static void Main() { Process test = Process.GetProcessesByName(\u0026#34;notepad\u0026#34;)[0]; PSS_CAPTURE_FLAGS flags = PSS_CAPTURE_FLAGS.PSS_CAPTURE_THREADS; PInvoke.PssCaptureSnapshot( test.SafeHandle, flags, 0, out HPSS snapshotHandle); PSS_PROCESS_INFORMATION info; // 添加错误码的声明 uint errorCode; unsafe { // 获取错误码返回值 errorCode = PInvoke.PssQuerySnapshot( snapshotHandle, PSS_QUERY_INFORMATION_CLASS.PSS_QUERY_PROCESS_INFORMATION, \u0026amp;info, (uint)sizeof(PSS_PROCESS_INFORMATION)); } Console.WriteLine(info.ProcessId); // 跟WIN32_ERROR.ERROR_SUCCESS做比较，返回True Console.WriteLine((WIN32_ERROR)errorCode == WIN32_ERROR.ERROR_SUCCESS); } } 当然头铁不改正确的声明也没关系，也是这样使用\n对于其他不知道怎么声明的枚举也可以这样让CsWin32去找\n","date":"2024-07-21T21:08:14+08:00","permalink":"/post/articles/2024/%E7%9B%B8%E5%AF%B9%E4%BC%98%E9%9B%85%E5%BE%97csharp%E8%B0%83%E7%94%A8win32api/","title":"相对优雅得C#调用Win32API"},{"content":"嘶，总算是学校的事情忙完了，终于有点时间写写博客了\n这个学期感觉一直都在和时间赛跑，没啥时间写博客，自己的项目也基本都断了\n当然这些都是另外的话题了\n如果之前有看过我博客的人，应该会发现现在网页大变样了\n现在博客的框架从Hexo换到了Hugo\n中间切换花了我很多时间，里面也有很多能说道说道的\n需要注意的是，我的部署方案跟官方的不一样，所以部分配置会和官方文档有所出入\n\u0026lt;0x00\u0026gt; 为什么要换框架 简单来说，也算是我闲着没事干\n当然，之前Hexo的流程不是很舒服，写博客的话图片很不好引用\n然后的话主题也不是很好看（我没时间去改主题）\n然后正好想尝试下新东西，然后就看到了Hugo，于是就试了试\n尝试之后发现这个框架使用体验确实舒适很多，于是就打算换框架了\n换了框架后，好处主要有以下几点：\n文章的图片资源可以更好的管理 Obsidian更好的融合 舒适的预览体验 \u0026lt;0x01\u0026gt; 安装Hugo与新建站点 安装Hugo 以windows为例，其他系统应该没差\n安装Hugo是很简单的\n如果有安装scoop的话，可以直接敲命令安装\nscoop install hugo-extended 这里安装的是带扩展的版本，因为我使用的主题需要这个\n如果有安装winget，也可以直接敲命令安装\nwinget install Hugo.Hugo.Extended 如果上面两个包管理器都没安装，那么需要从Release中下载预构建的版本\n找到带windows的压缩包下载，里面会有一个hugo.exe\n然后丢到什么路径，添加到windows环境变量里面，这样就可以在命令行中调用了\n新建Hugo站点 完成安装后，尝试新建站点来测试是否安装成功\n在希望存放站点内容的位置打开终端\nhugo new site TestBlog 这样会在这个位置创建叫TestBlog的文件夹，里面会包含hugo站点的结构\n以上就完成了Hugo站点的搭建\n\u0026lt;0x02\u0026gt; 配置Stack主题 这里使用的主题是Stack，简洁好看\n路径约定 ./表示存放所有Hugo站点的路径\n所以./TestBlog就是存放TestBlog站点的路径\n本文后面所有的路径都将按此表示\n安装Stack主题 ./TestBlog/themes是存放所有主题的路径\n在此打开终端\ngit clone https://github.com/CaiJimmy/hugo-theme-stack.git 克隆主题仓库\n然后回到./TestBlog，里面有个hugo.toml\n将theme的值修改为'hugo-theme-stack'\n配置头像 进入./TestBlog/themes/hugo-theme-stack\n这里是Stack主题的文件\n我们需要修改的头像在./TestBlog/themes/hugo-theme-stack/assets/img中\n那个avatar.png便是我们我修改的图像\n找自己的头像拖进去然后改名就可以了\n配置网站图标 配置网站图标稍微麻烦点\n首先进入./TestBlog/themes/hugo-theme-stack\n创建叫static的文件夹，里面放入网站图标，这里取名favicon.webp\n然后回到./TestBlog/themes/hugo-theme-stack，打开文件config.yaml\n找到favicon，将值改为favicon.webp，也就是网站图标的文件名\n配置社交媒体菜单 （指的是这个东西）\n在./TestBlog/themes/hugo-theme-stack，打开文件config.yaml\n在文件最后添加这些东西\nmenu: main: [] social: - identifier: github name: 起个名吧 url: GitHub链接 params: icon: brand-github - identifier: bilibili name: 起个名吧 url: bilibili链接 params: icon: brand-bilibili 但仅仅这样是不行的，如果构建网页会说没有brand-bilibili.svg\n所以我们需要添加这个svg\n进入./TestBlog/themes/hugo-theme-stack/assets/icons，里面是所有的图标文件\n新建一个文件，命名为brand-bilibili.svg，添加以下内容\n\u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; width=\u0026#34;24\u0026#34; height=\u0026#34;24\u0026#34; viewBox=\u0026#34;0 0 24 24\u0026#34; fill=\u0026#34;none\u0026#34; stroke=\u0026#34;currentcolor\u0026#34; stroke-width=\u0026#34;2\u0026#34; stroke-linecap=\u0026#34;round\u0026#34; stroke-linejoin=\u0026#34;round\u0026#34; class=\u0026#34;icon icon-tabler icons-tabler-outline icon-tabler-brand-bilibili\u0026#34;\u0026gt; \u0026lt;path stroke=\u0026#34;none\u0026#34; d=\u0026#34;M0 0h24v24H0z\u0026#34; fill=\u0026#34;none\u0026#34;\u0026gt;\u0026lt;/path\u0026gt; \u0026lt;path d=\u0026#34;M3 10a4 4 0 014-4h10a4 4 0 014 4v6a4 4 0 01-4 4H7a4 4 0 01-4-4v-6z\u0026#34;\u0026gt;\u0026lt;/path\u0026gt; \u0026lt;path d=\u0026#34;M8 3l2 3\u0026#34;\u0026gt;\u0026lt;/path\u0026gt; \u0026lt;path d=\u0026#34;M16 3l-2 3\u0026#34;\u0026gt;\u0026lt;/path\u0026gt; \u0026lt;path d=\u0026#34;M9 13v-2\u0026#34;\u0026gt;\u0026lt;/path\u0026gt; \u0026lt;path d=\u0026#34;M15 11v2\u0026#34;\u0026gt;\u0026lt;/path\u0026gt; \u0026lt;/svg\u0026gt; （从别人博客薅的）\n保存即可\n如果要添加其他网站图标也是这样\n\u0026lt;0x03\u0026gt; 文章路径与侧栏 Hugo的所有文章都是放在./TestBlog/content里面\nHugo支持页面包来对文章资源进行管理\n页面包就是按路径对文章进行打包，一个文章只能访问自己页面包内的内容\n这样就将每个文章独立开来，不像之前Hexo所有图片都在一个文件夹中\n（或许Hexo也有办法实现，但我不知道）\n修改默认文章目录 Stack主题默认指定./TestBlog/content/post为文章目录\n这里我改成了./TestBlog/content/article\n在./TestBlog/themes/hugo-theme-stack，打开文件config.yaml\n找到mainSections将值改为article即可\n侧栏部分 这里的配置建议参考Stack主题给的exampleSite配置\n直接按page里面的结构配置即可\n文章路径参考 我这的文章路径是这样的\ncontent\r├── article\r| ├── posts\r| | ├── 随便起的文章名字\r| | | ├── picture\r| | | | └── 1.png\r| | | └── index.md\r| | ├── ...\r| | └── ...\r| └── categories\r| ├── Blog\r| | ├── 随便起的文章名字\r| | | ├── picture\r| | | | └── 1.png\r| | | └── index.md\r| | ├── ...\r| | └── ...\r| ├── ...\r| └── ...\r├── page\r| ├── about\r| | └── index.md\r| ├── archives\r| | └── index.md\r| ├── links\r| | └── index.md\r| └── search\r| └── index.md\r└── _index.md \u0026lt;0x04\u0026gt; obsidian配置 用obsidian在./TestBlog/content创建仓库\nobsidian里面需要配置的东西不多，主要是把链接设置为相对文件的路径\n这样配置就可以了\n由于使用页面包进行管理，所以所有的文章文件名都是index.md\n这个会导致obsidian节点视图之类的功能显示的都是index，我目前没有很好的办法\n\u0026lt;0x05\u0026gt; 部署到Github Page上 虽然官方文档有讲，但他们采用的是将整个站点push到github上\n然后采用Github Action来在上面构建网页\n这样的话如果Action脚本有误可能导致github page行为和本地测试不一样\n我打算本地构建，然后将构建好的网页push到github上面，这样逻辑跟Hexo那套差不多\n但Hugo没有这样的自动部署配置，所以要自己来\n大致流程是在./TestBlog/public中初始化git仓库\n添加远程git仓库\n拉取后提交所有更改\n如果需要切换分支的话再切换分支\n之后只要构建后提交更改并上传即可\n具体命令参考下面的自动化脚本\n\u0026lt;0x06\u0026gt; 通过PowerShell实现自动化 因为是在windows平台，所以这里采用PowerShell来实现一些自动化\n统一变量配置 因为会有好几个脚本，所以需要一个统一的变量配置\n# configs.ps1 # 获取脚本文件夹路径 $global:ShellScriptDir = Get-Location # 主目录 Set-Location .. $global:SiteDir = Get-Location Set-Location $global:ShellScriptDir # 内容目录 $global:ContentDir = Join-Path -Path $global:SiteDir -ChildPath \u0026#34;content\u0026#34; # 文章目录 $global:ArticleDir = Join-Path -Path $global:ContentDir -ChildPath \u0026#34;article\u0026#34; # 一般文章目录 $global:PostsDir = Join-Path -Path $global:ArticleDir -ChildPath \u0026#34;posts\u0026#34; # 分类文章目录 $global:CategoriesDir = Join-Path -Path $global:ArticleDir -ChildPath \u0026#34;categories\u0026#34; # 基础URL $global:BaseURL = \u0026#34;[...]\u0026#34; # 远程Git仓库链接 $global:RemoteGitURL = \u0026#34;[...]\u0026#34; # 目标分支 $global:TargetBranch = \u0026#34;[...]\u0026#34; # 编辑器路径 $global:EditorPath = \u0026#34;[...]\u0026#34; 创建文章 因为希望每次创建文章都会自动创建对应的页面包，所以也需要一个脚本\n# NewPost.ps1 param( [string] [ValidateNotNullOrEmpty()] $PostName, [string] $CategoryName, [bool] $IsStartServer = $false, [bool] $IsCreatePictureFolder = $false ) function ValidatedName { param ( [string] [ValidateNotNullOrEmpty()] $Name ) # 其他的特殊字符 $SpecialCharacters = [char[]]@( \u0026#34;` \u0026#34; \u0026#34;`!\u0026#34; \u0026#34;`$\u0026#34; \u0026#34;`%\u0026#34; \u0026#34;`\u0026amp;\u0026#34; \u0026#34;`\u0026#39;\u0026#34; \u0026#34;(\u0026#34; \u0026#34;)\u0026#34; \u0026#34;*\u0026#34; \u0026#34;,\u0026#34; \u0026#34;-\u0026#34; \u0026#34;/\u0026#34; \u0026#34;:\u0026#34; \u0026#34;;\u0026#34; \u0026#34;\u0026lt;\u0026#34; \u0026#34;=\u0026#34; \u0026#34;\u0026gt;\u0026#34; \u0026#34;?\u0026#34; \u0026#34;@\u0026#34; \u0026#34;[\u0026#34; \u0026#34;]\u0026#34; \u0026#34;^\u0026#34; \u0026#34;_\u0026#34; \u0026#34;``\u0026#34; \u0026#34;{\u0026#34; \u0026#34;|\u0026#34; \u0026#34;}\u0026#34; \u0026#34;~\u0026#34; ) # 因为我会写C#相关的文章，#有一些意义，对#做特殊替换 $Name = $Name.Replace(\u0026#34;`#\u0026#34;, \u0026#34;Sharp\u0026#34;) # 先每个替换成- foreach ($Character in $SpecialCharacters) { $Name = $Name.Replace($Character, \u0026#34;-\u0026#34;) } # 返回时使用正则表达式匹配连续的-，并替换为单个- return $Name -Replace \u0026#39;([-])+\u0026#39;, \u0026#39;-\u0026#39; } # 保存当前的工作路径 $localDir = Get-Location # 导入配置变量 . .\\configs.ps1 # 切换工作路径 Set-Location $global:SiteDir # 合法化路径名，不然图片会出问题 $validPath = ValidatedName -Name $PostName # 普通文章创建 if ($CategoryName -eq \u0026#34;\u0026#34;) { # 新建Hugo文章 $newPostPath = Join-Path -Path \u0026#34;content\\article\\posts\u0026#34; -ChildPath (Get-Date -Format \u0026#34;yyyy\u0026#34;) $newPostPath = Join-Path -Path (Join-Path -Path $newPostPath -ChildPath $validPath) -ChildPath \u0026#34;index.md\u0026#34; hugo new $newPostPath # 生成图片文件夹路径 $newPostPicture = Join-Path -Path (Join-Path -Path $global:PostsDir -ChildPath $validPath) -ChildPath \u0026#34;picture\u0026#34; } # 需要分类的文章 else { # 新建Hugo文章 $categoryPath = Join-Path -Path \u0026#34;content\\article\\categories\u0026#34; -ChildPath $CategoryName $newPostPath = Join-Path -Path (Join-Path -Path $categoryPath -ChildPath $validPath) -ChildPath \u0026#34;index.md\u0026#34; hugo new $newPostPath # 生成图片文件夹路径 $categoryPath = Join-Path -Path $global:CategoriesDir -ChildPath $CategoryName $newPostPicture = Join-Path -Path (Join-Path -Path $categoryPath -ChildPath $validPath) -ChildPath \u0026#34;picture\u0026#34; } # 需要创建图片文件夹的话就创建 if ($IsCreatePictureFolder) { mkdir $newPostPicture } # 启动编辑器 # Start-Process $global:EditorPath # 是否开启服务器预览 if ($IsStartServer) { hugo server } # 返回之前的工作路径 Set-Location $localDir 使用示例\n# 创建一篇普通的文章 .\\NewPost.ps1 -PostName 从Hexo到Hugo-记录博客框架切换 # 创建一篇文章，并分类为Blog .\\NewPost.ps1 -PostName 从Hexo到Hugo-记录博客框架切换 -CategoryName Blog 运行后会创建对应的页面包\n发布 这里涉及到两个文件，一个InitGit.ps1用来初始化git仓库，一个Deploy.ps1做推送\nInitGit.ps1如下\n# InitGit.ps1 param( [bool] $IsForcePush = $false ) # 保存当前的工作路径 $localDir = Get-Location # 导入配置变量 . .\\configs.ps1 # 切换工作路径 Set-Location (Join-Path -Path $global:SiteDir -ChildPath \u0026#34;public\u0026#34;) # 初始化git git init git remote add origin $global:RemoteGitURL git pull git add . git commit -m (\u0026#34;Init: \u0026#34; + (Get-Date -Format \u0026#34;yyMMdd-HH:mm\u0026#34;)) git branch -u $global:TargetBranch if ($IsForcePush) { git push origin HEAD:$global:TargetBranch --force } # 返回之前的工作路径 Set-Location $localDir Deploy.ps1如下\n# Deploy.ps1 param( [bool] $IsForcePush = $false, [bool] $IsClearDeploy = $false ) # 保存当前的工作路径 $localDir = Get-Location # 导入配置变量 . .\\configs.ps1 # 切换工作路径 Set-Location $global:SiteDir # 清除Hugo构建缓存 if ($IsClearDeploy) { # 递归删除所有文件 Remove-Item -Path (Join-Path -Path $global:PublicDir -ChildPath \u0026#34;*\u0026#34;) -Recurse } # 构建网页 hugo --gc --minify --baseURL $global:BaseURL # 准备推送 Set-Location $global:PublicDir # 没有初始化git的化就先初始化并强制推送 if (-not (Test-Path \u0026#34;.git\u0026#34;)) { . (Join-Path -Path $global:ShellScriptDir -ChildPath \u0026#34;InitGit.ps1\u0026#34;) -IsForcePush $true } else { git add . git commit -m (\u0026#34;Deploy: \u0026#34; + (Get-Date -Format \u0026#34;yyMMdd-HH:mm\u0026#34;)) # 是否强制推送 if ($IsForcePush) { git push origin HEAD:$global:TargetBranch --force } else { git push origin HEAD:$global:TargetBranch } } # 返回之前的工作路径 Set-Location $localDir 使用示例\n# 正常的发布 .\\Deploy.ps1 # 清空所有构建缓存发布 .\\Deploy.ps1 -IsClearDeploy 运行即可推送到GitHub Page上\n批量创建页面包路径结构 因为之前使用的是hexo，里面都是单独的markdown文件\n如果自己手动一个一个移动并创建picture文件夹那就太累了\n所以还是要写一个脚本完成自动化\n# TransFromSingleFile.ps1 param( [string] [ValidateNotNullOrEmpty()] $postDir ) # 排除文件名单 $excludeList = \u0026#34;_index\u0026#34; Get-ChildItem -Path $postDir -File | ForEach-Object { if ($_.BaseName -notin $excludeList) { # 创建文件夹并移动 $destDir = Join-Path -Path $postDir -ChildPath $_.BaseName New-Item -ItemType Directory -Force -Path $destDir Move-Item -Path $_.FullName -Destination (Join-Path -Path $destDir -ChildPath \u0026#34;index$($_.Extension)\u0026#34;) # 创建对应的picture文件夹 $newPostPicture = Join-Path -Path $destDir -ChildPath \u0026#34;picture\u0026#34; mkdir $newPostPicture } } 使用示例\n.\\TransFromSingleFile.ps1 [某个路径] 运行可以为路径内所有文件创建同名文件夹，并将文件移动至对应文件夹后重命名为index.md\n里面也会创建picture文件夹\n","date":"2024-07-20T15:02:34+08:00","permalink":"/post/categories/blog/%E4%BB%8Ehexo%E5%88%B0hugo%E8%AE%B0%E5%BD%95%E5%8D%9A%E5%AE%A2%E6%A1%86%E6%9E%B6%E5%88%87%E6%8D%A2/","title":"从Hexo到Hugo：记录博客框架切换"},{"content":"今天我们神秘的数字图像处理做实验了\n我使用C#配合Opencvsharp4做实验\n虽然Opencvsharp4宣称是和C++的API是对应的\n但实际上的开发体验差别真挺大的，很多API的调用方法是不一样的\n（有些调用像C++，有些调用有自己的特色）\n基本上所有的代码都要自己去试，写着挺痛苦的\n所以还是讲讲我们做了啥吧\n\u0026lt;0x00\u0026gt;读取图像 这个其实是简单的\n假设需要读取一个图片，比方说是D:/Test/1.png\n通过Cv2.ImRead()方法就可以读取为内置的矩阵类型了\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); 这个方法可以读取一般常见的图片格式\n然后，这个方法还可以传入第二个参数，用来控制读取的行为\n比方说我们希望获取的是图像的灰度图\nMat grayImage = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;, ImreadModes.Grayscale); ImreadModes是一个枚举类型，有很多常用的读取行为\n一般也就是这个ImreadModes.Grayscale获取灰度图用的多\nCv2.ImRead()这个参数的默认值是ImreadModes.Color，即默认读入带颜色的图像\n\u0026lt;0x01\u0026gt;将有色图转换为灰度图 前面提到，我们可以指定读取图片时保存为灰度图\n那有没有别的在运行时从有色图转换为灰度图的方法呢？\n肯定是有的\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat grayImage = iamge.CvtColor(ColorConversionCodes.RGB2GRAY); 这样也完成了转换\n还有一个等价的写法\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat grayImage = new(); Cv2.CvtColor(image, grayImage, ColorConversionCodes.RGB2GRAY); 这样写也行\nCvtColor()方法中，传入的ColorConversionCodes也是个枚举类型\n里面也有很多常用的转换方式，有什么RGB2BGR之类的\n\u0026lt;0x02\u0026gt;图像按位取反 这个在C#中的写法确实跟C++之类的挺像的\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat inversedImage = new(); Cv2.BitwiseNot(image, inversedImage); 当然也可以自己写对每个位的操作，但这样就太麻烦了\n\u0026lt;0x03\u0026gt;图像对比度增强 关于图像对比度增强的办法有很多，我这里用的是限制对比度自适应直方图均衡化的办法\n（原理我也说不出来，没细学，还是看别的大佬的文章吧）\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat enhancedImage = new(); CLAHE clahe = Cv2.CreateCLAHE(3, new OpenCvSharp.Size(8, 8)); clahe.Apply(image, enhancedImage); 第一个参数表示像素出现的阈值，应该是百分比阈值，默认是40\n第二个参数表示重新分配的大小\n限制对比度自适应直方图均衡化的大体过程如下\n首先会统计每种像素的出现次数\n然后跟设置的阈值对比，如果超过阈值，会把多余的像素按设定的大小重新分配给别的类型的像素\n总之确实是能增强对比度（真说不大明白）\n\u0026lt;0x04\u0026gt;图像二值化 所谓二值化算是极端化的灰度图，只有黑白两种颜色\n代码如下\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat binarizedImage = image.Threshold(127, 255, ThresholdTypes.Binary); 也有稍微麻烦点的写法\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat binarizedImage = new(); Cv2.Threshold(image, binarizedImage, 127, 255, ThresholdTypes.Binary); Threshold()第一个数字是阈值，第二个值算是默认值，具体行为看第三个参数\nThresholdTypes是一个枚举类型，表示如何处理图像\n二值化的时候这个值取ThresholdTypes.Binary，表示像素值大于阈值时用默认值，否则赋值0\n比方说在二值化的时候，某像素值为128\u0026gt;127，那么就会把它的值变为255\n其他的行为这里就不展开了\n\u0026lt;0x05\u0026gt;图片相加 同大小图片相加 这个其实很简单，毕竟在Opencv眼里，所有的图片都是矩阵，相加不是很简单嘛\nMat A = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat B = Cv2.ImRead(\u0026#34;D:/Test/2.png\u0026#34;); Mat addedImage = A + B; （就这样写就可以了）\n不嫌烦的话还有下面的写法\nMat addedImage = A.Add(B); Mat addedImage = new(); Cv2.Add(A, B, addedImage); 不同大小的图片相加 这些Add()方法要求矩阵大小一致，相当于要求图片大小一致\n那如果我们就是要不同大小图片相加，就要多做些处理\n我的处理方式是\n先在比较大的图片中扣出跟小图片一样大小的部分\n抠出的图和小图片相加，然后再替换掉大图片扣掉的部分\n怎么抠出图像 Opencvsharp4中有个Rect类型，可以用来划出需要的部分\n类型声明示例\nRect rect = new(0, 0, 100, 100); 前面表示起始位置，也即矩形的左上角的点坐标，后面两个参数表示矩形大小\n对于一个图像，假设我们需要在图像(100, 100)的地方扣出200*200大小的图片\n代码如下\nMat image = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat temp = image[new Rect(100, 100, 200, 200)]; 需要注意的是，这里的temp获取到的是image对应区域的引用\n对temp的所有修改也会反应到image上\n如果需要新建一个矩阵，需要用CopyTo()方法\n完整的代码 有了上面的基础，现在给出完整的图片相加代码\nMat L = Cv2.ImRead(\u0026#34;D:/Test/1.png\u0026#34;); Mat S = Cv2.ImRead(\u0026#34;D:/Test/2.png\u0026#34;); //还需要注意框选的区域不能超过大图片的范围 Mat temp = image[new Rect(100, 100, S.Width, S.Height)]; temp = temp + s; 这样就可以了，至于图片相减也是差不多这样的代码\n","date":"2024-03-18T12:59:53Z","permalink":"/post/articles/2024/%E5%85%B3%E4%BA%8E%E6%88%91%E7%94%A8csharp%E5%86%99opencv%E8%BF%99%E4%BB%B6%E4%BA%8B/","title":"关于我用C#写Opencv这件事"},{"content":"最近在写一个WPF项目，然后今天遇到个非常神秘的问题 就是界面的InitializeComponent()莫名奇妙报错找不到方法了 我仔细检查代码，也没发现问题 而且最抽象的是，程序也能正常运行 我用ILSpy去看编译后的文件，发现InitializeComponent()也是有的\n但报错看着总是不舒服啊，还是要解决 参考网上的说法，进入项目的文件夹 然后把obj文件夹删掉，再在vs里面重新生成一下就好了 据说是因为分布类的问题，实际编译时生成的代码是正确的 但是IntelliCode依赖的文件可能没更新上 然后就开始误报找不到InitializeComponent()\n总之还是一个很神秘的问题\n","date":"2024-03-07T01:10:14Z","permalink":"/post/categories/wpf/wpf%E5%BC%80%E5%8F%91%E8%BF%87%E7%A8%8B%E4%B8%ADinitializecomponent-%E7%A5%9E%E7%A7%98%E6%8A%A5%E9%94%99/","title":"WPF开发过程中InitializeComponent()神秘报错"},{"content":"闲着没事想着优化一下hexo的博客配置 我原来一直都是用vscode写markdown的，至于体验嘛，能写但不是很舒适 所以我就尝试换成了现在在用的obsidian，体验就好多了\n然后就按网上的教程，尝试配置更加优雅的流程 然后发现原来用的hexo框架有点老了，就干脆重装了 没想到啊，重装个hexo又出了很多问题 所以这篇博客就从hexo的安装开始讲吧\n\u0026lt;0x00\u0026gt; hexo的安装 （一般情况下跟着hexo官方教程没啥问题） 首先是要有Node.js环境，这个随意，最好是最新的版本 还要安装git，这个直接网上找安装包安装就好了\nnpm换源 npm原本的国内连接不畅，所以要换源 一般是换成国内的淘宝源\n一般敲命令方法 npm config set registry https://registry.npm.taobao.org 通过nrm切换 nrm是npm的源管理器，可以在不同的源中切换 如果没有什么特殊上网方式访问原本的源的话还是建议先手动切换源 （nrm也是靠npm下载的）\n# -g参数表示全局安装，这样的话可以在任何目录中使用nrm npm install -g nrm nrm use taobao # 查看其它可以用的源 nrm ls # 测试源 nrm test 安装hexo 首先是安装hexo环境\nnpm install -g hexo-cli 然后挑一个空的文件夹，初始化 （这里取名叫HexoBlog了）\nhexo init ./HexoBlog 然后原本空的文件夹里面会生成一些文件 安装hexo依赖\ncd ./HexoBlog npm install 这样之后，hexo就搭建好了 但貌似hexo的依赖文件写的版本可能比较老了，最好还是更新一下\nnpm update \u0026lt;0x01\u0026gt; 安装Anatolo主题 别的主题就看具体的安装说明了，我这里还是用Anatolo了\n首先是在HexoBlog文件夹\ngit clone https://gitee.com/Lhcfl/hexo-theme-anatolo.git themes/Anatolo npm install hexo-renderer-pug --save npm install hexo-renderer-stylus --save 然后进入HexoBlog/themes/Anatolo中，复制_config.example.yml为_config.yml 这个_config.yml是Anatolo的配置文件 这里还需要多做一步，这一步在原本的安装文件中没有说\nnpm install 需要在Anatolo中再安装一次依赖，如果没有这一步的话运行时会报错找不到模块 我的猜想是我们在安装hexo依赖时没有全局安装，所以要在这里再安装一次 （或者所有的npm都-g全局安装也行，但这样对C盘不友好，尤其是像我并不是专门做Nodejs开发的人）\n然后回到HexoBlog，修改_config.yml中的theme: Anatolo 这样主题也安装好了\n\u0026lt;0x02\u0026gt; 优化Hexo配置 部署到Github Page上 这个网上的教程足够多了，这边不细讲 就是记得安装hexo-deployer-git插件就好 在HexoBlog中\nnpm install hexo-deployer-git --save 更方便的分文件管理 hexo的默认所有的博客文章都存放在HexoBlog/source/_posts下面 虽然可以在里面用文件夹归类不同的文章，但需要给这些博客手动写上categories属性 这就很麻烦了，手写麻烦还容易出错\n可以安装一个插件，hexo-auto-category 它可以在hexo生成网页时自动给每个博客打上对应的categories属性\n在HexoBlog中\nnpm install hexo-auto-category --save 修改_config.yml，添加下面的语句\nauto_category: enable: true depth: 这样就安装好并启用了\n\u0026lt;0x03\u0026gt; 使用obsidian作为编辑器 obsidian确实香，比起原本的vscode好多了，毕竟是专门的markdown编辑器 但要在hexo中使用obsidian作为编辑器，还需要做一些小优化\n仓库的话，用obsidian打开HexoBlog/source/_posts就可以了\n图片问题 hexo在生成时，图片需要在HexoBlog/source下面 但由于obsidian打开的目录在HexoBlog/source/_posts，所以会检索不到图片\n我的解决方案是在HexoBlog/source下面新建picture文件夹来专门存图片 然后在HexoBlog/source/_posts下面添加picture软连接，让obsidian可以检索到\n添加软连接的命令在powershell下和cmd下面有所不同，但都需要管理员权限\n# powershell New-Item -ItemType SymbolicLink -Path $放的地方 -Name $名字 -Target $连接的目标 # cmd mklink /d $软连接存放路径 $连接的目标 然后就是在obsidian中编辑时，引用图片的格式应是![](/picture/...) 最好把obsidian的文件与链接\u0026gt; \u0026gt; 自动添加wiki引用关了，[[]]格式的连接hexo本身解析不了 （貌似有插件在解决这个问题，但我没试成功过）\n\u0026lt;0x04\u0026gt; 使用脚本做自动化 hexo的生成和部署都还是靠cli命令的 所以可以写一些脚本来做一些自动化 这里的脚本都是powershell脚本\n新建博客文章并编辑 # (@24-03-07) 优化脚本，增加去除URL特殊字符 param( [string] [ValidateNotNullOrEmpty()] $Name ) function ValidatedName { param ( [string] [ValidateNotNullOrEmpty()] $Name ) $SpecialCharacters = [char[]]@( \u0026#34;` \u0026#34; \u0026#34;`!\u0026#34; \u0026#34;`\u0026#34;\u0026#34; \u0026#34;`#\u0026#34; \u0026#34;`$\u0026#34; \u0026#34;`%\u0026#34; \u0026#34;`\u0026amp;\u0026#34; \u0026#34;`\u0026#39;\u0026#34; \u0026#34;(\u0026#34; \u0026#34;)\u0026#34; \u0026#34;*\u0026#34; \u0026#34;+\u0026#34; \u0026#34;,\u0026#34; \u0026#34;-\u0026#34; \u0026#34;.\u0026#34; \u0026#34;/\u0026#34; \u0026#34;:\u0026#34; \u0026#34;;\u0026#34; \u0026#34;\u0026lt;\u0026#34; \u0026#34;=\u0026#34; \u0026#34;\u0026gt; \u0026#34; \u0026#34;?\u0026#34; \u0026#34;@\u0026#34; \u0026#34;[\u0026#34; \u0026#34;\\\u0026#34; \u0026#34;]\u0026#34; \u0026#34;^\u0026#34; \u0026#34;_\u0026#34; \u0026#34;``\u0026#34; \u0026#34;{\u0026#34; \u0026#34;|\u0026#34; \u0026#34;}\u0026#34; \u0026#34;~\u0026#34; ) # 先每个替换成- foreach ($Character in $SpecialCharacters) { $Name = $Name.Replace($Character, \u0026#34;-\u0026#34;) } # 返回时使用正则表达式匹配连续的-，并替换为单个- return $Name -Replace \u0026#39;([-])+\u0026#39;, \u0026#39;-\u0026#39; } # 指定Obsidian目录 $ObsidianPath = \u0026#34;C:\\Users\\cookie\\scoop\\apps\\obsidian\\current\\Obsidian.exe\u0026#34; # 新建hexo博客 hexo new $Name # 去除URL特殊字符 $Name=ValidatedName -Name $Name # 创建图片文件夹 mkdir ./source/picture/$Name # 启动Obsidian Start-Process $ObsidianPath -ArgumentList ./source/_posts # cls Clear-Host 本地化部署测试 param( [Int16] $Port ) # 清理hexo缓存 hexo clean # hexo生成 hexo g # 启动本地hexo服务器 if ($Port -eq $null) { hexo server } else{ hexo server -p $Port } \u0026lt;0x05\u0026gt; 安装过程中可能的问题 hexo的有些依赖可能已经已放弃维护了 比如说hexo-renderer-sass，这个非常容易报错 （貌似删了不影响整体运行） 可以替换成hexo-renderer-sass-next，这个稳定很多\n\u0026lt;0x06\u0026gt; 这套方案的问题 不支持Obsidian的wiki链接解析 也没用上Obsidian自带的模板功能 文章之间相互引用也很麻烦，要知道文章的具体URL\n","date":"2024-03-03T13:24:40Z","permalink":"/post/categories/blog/%E6%88%91%E7%9A%84hexo%E9%85%8D%E5%90%88obsidian%E7%9A%84%E9%85%8D%E7%BD%AE/","title":"我的hexo配合obsidian的配置"},{"content":"我经常会开一堆网页，如果采用水平的标签页的话会经常找不到标签页\n而火狐又不像别的浏览器，起码到目前为止都没有官方的垂直标签页的支持\n经过一番探索，实际上是可以自己改出来的\n\u0026lt;0x00\u0026gt; 效果展示 效果还是很不错的\n\u0026lt;0x01\u0026gt; 安装Tree Style Tab插件 这是一个能在侧栏以树的形式管理标签页的插件，功能还挺多的\n这个插件在火狐的插件商店里就有\n别的类似的插件应该也有，但这里就用这个插件了\n建议先熟悉下这个插件的操作再做下面的操作\n\u0026lt;0x02\u0026gt; 打开about:config的一个选项 首先要打开about:config的关于自定义界面的选项\n火狐会跳出提示，别动别的东西就好\n搜索toolkit.legacyUserProfileCustomizations.stylesheets，把值改成true即可 \u0026lt;0x03\u0026gt; 打开标题栏 在火狐顶部右键，找到定制工具栏\n（图片右上角）\n然后打开左下角的标题栏\n这样做是为了在隐藏顶栏后还能做拖动窗口之类的操作\n\u0026lt;0x04\u0026gt; 找到用户的profile文件夹 这个也简单，Win+R输入%APPDATA%\\Mozilla\\Firefox\\Profiles\\\n里面可能会有多个名字很乱的文件夹，问题不大，找到里面文件最多的文件夹\n那个就是存用户设置的地方\n（我这里是这样的）\n\u0026lt;0x05\u0026gt; 创建修改界面的文件 在刚刚找到的文件夹里新建叫chrome的文件夹\n然后在chrome文件夹里面新建一个userChrome.css的文件\n打开编辑，输入下面的代码\n/*侧边栏头部是否显示，会影响书签侧栏之类的，可以不写*/ #sidebar-header { display: none !important; } /*隐藏顶栏，也就是原来的标签栏*/ #TabsToolbar { visibility: collapse !important; } 然后重启火狐理论上就生效了\n","date":"2024-01-02T21:34:53Z","permalink":"/post/articles/2024/%E5%A6%82%E4%BD%95%E5%B0%86firefox%E6%94%B9%E6%88%90%E5%9E%82%E7%9B%B4%E6%A0%87%E7%AD%BE%E9%A1%B5/","title":"如何将Firefox改成垂直标签页"},{"content":"什么是SharpPcap 最近嘛，要写各种课设，计网也要\n课设就算了，里面还有一个网络编程\n要求捕获些IP数据报，并输出些IP数据报里面的一些数据\n然后一看要求，霍，好家伙，能用C#，那就简单了\n说回正题，要了解什么是SharpPcap，就要先了解WinPcap\nWinPcap是在windows下抓包的工具集合，相当于一个抓包的api\n有了这个，我们就不需要直接与网卡打交道了，要抓包，直接调WinPcap提供的api\nSharpPcap就是把这些api打包成C#的形式，让我们可用在C#中方便的抓包\n安装 这个就很简单了，直接到Nuget包管理器里面安装就是了\n(@ 24-01-11)\n其实应该用NPcap的，这个比较新，WinPcap有点老了\nWinPcap调用不了无线网卡 这两个SharpPcap都可以调用\nNPcap下载\n在写代码前，还要检查下又没有安装过WinPcap，没有的话会找不到捕获的设备\nWinPcap下载\n这个也是装上就完事了\n写代码 需要注意的是，SharpPcap貌似重构过好多次，我看到的使用范例有好几版\n但都没用，最后还是靠我自己反编译看的\n(GitHub那里的范例也是不能直接用的)\n总之，我这里用的是6.2.5的版本，别的版本我不管\n丢一段代码，里面带着详细的注释(这段代码也是我计网课设网络编程的代码)\n(@ 24-01-11)\n稍微修改下，原来的代码在处理标识符的逻辑点问题\n(转成BitArray会出现高低位顺序的问题)\nusing System; using SharpPcap; using PacketDotNet; using System.Collections; namespace Test { public class Program { static void Main() { CaptureDeviceList devices = CaptureDeviceList.Instance;//获取所有可用的设备 if (devices.Count \u0026lt; 1) { Console.WriteLine(\u0026#34;No devices were found on this machine\u0026#34;); return; }//没有就直接退出了 Console.WriteLine(\u0026#34;\\nThe following devices are available on this machine:\u0026#34;); Console.WriteLine(\u0026#34;----------------------------------------------------\\n\u0026#34;); for (int i = 0; i \u0026lt; devices.Count; i++) { Console.WriteLine($\u0026#34;index:{i}|{devices[i].Description}\\n\u0026#34;); }//输出可用的设备及其编号 string num = Console.ReadLine();//获取用户输入的编号 ICaptureDevice device = devices[int.Parse(num)];//获取对应的设备对象 device.Open(DeviceModes.Promiscuous, 10000);//准备启动设备 string filter = \u0026#34;ip\u0026#34;; device.Filter = filter;//设置过滤器为ip数据报 device.OnPacketArrival += Device_OnPacketArrival;//设置收到包后的回调方法 device.StartCapture();//开始捕捉 Console.ReadLine(); device.StopCapture();//如果按下任意键，终止捕捉 device.Close();//释放设备 } private static void Device_OnPacketArrival(object sender, PacketCapture e) { var ip = e.GetPacket().GetPacket().Extract\u0026lt;IPPacket\u0026gt;();//把捕捉到的包转换成ip数据报 //因为库里没有处理标识符和片偏移的逻辑，这里要自己写处理逻辑 var s = new BitArray(new byte[] { ip.HeaderData[5], ip.HeaderData[4] });//获取第5第6字节的数据 var symbol = new BitArray(new byte[] { ip.HeaderData[7], ip.HeaderData[6] });//获取第7第8字节的数据 Console.WriteLine(); Console.WriteLine($\u0026#34;Version:\\t{ip.Version}\u0026#34;); Console.WriteLine($\u0026#34;Length:\\t\\t{ip.TotalLength}\u0026#34;); Console.WriteLine($\u0026#34;Serial:\\t\\t{BitArrayToInt(s, 0, 15)}\u0026#34;); Console.WriteLine($\u0026#34;DF:\\t\\t{symbol[14]}\u0026#34;); Console.WriteLine($\u0026#34;MF:\\t\\t{symbol[13]}\u0026#34;); Console.WriteLine($\u0026#34;Offest:\\t\\t{BitArrayToInt(symbol, 0, 12)}\u0026#34;); Console.WriteLine($\u0026#34;From:\\t\\t{ip.SourceAddress}\u0026#34;); Console.WriteLine($\u0026#34;To:\\t\\t{ip.DestinationAddress}\u0026#34;); Console.WriteLine($\u0026#34;Protocol:\\t{ip.Protocol}\u0026#34;); for (int i = 0; i \u0026lt; ip.HeaderData.Length; i++) { if (ip.HeaderData[i] \u0026lt; 16) { Console.Write(\u0026#34;0{0:X} \u0026#34;, ip.HeaderData[i]); } else { Console.Write(\u0026#34;{0:X} \u0026#34;, ip.HeaderData[i]); } if (i % 4 == 3) { Console.WriteLine(); } } Console.WriteLine(); } static int BitArrayToInt(BitArray bitArray, int head, int end)//只是个把二进制数组转换成数字的方法 { int a = 0; for (int i = head; i \u0026lt;= end; i++) { if (bitArray[i]) { a += 1; } a *= 2; } return a; } } } 实际上确实就这么一段代码就够了\n里面的注释也已经点明了具体的功能\n需要注意的是，这里通过事件机制来处理收到包的情况\n也就是这一句\ndevice.OnPacketArrival += Device_OnPacketArrival;//设置收到包后的回调方法 在device收到数据报后，会触发OnPacketArrival事件\n然后就可用用事件那一套机制来处理收到的包了，也就是这一段代码\nprivate static void Device_OnPacketArrival(object sender, PacketCapture e) { var ip = e.GetPacket().GetPacket().Extract\u0026lt;IPPacket\u0026gt;();//把捕捉到的包转换成ip数据报 //因为库里没有处理标识符和片偏移的逻辑，这里要自己写处理逻辑 var s = new BitArray(new byte[] { ip.HeaderData[5], ip.HeaderData[4] });//获取第5第6字节的数据 var symbol = new BitArray(new byte[] { ip.HeaderData[7], ip.HeaderData[6] });//获取第7第8字节的数据 Console.WriteLine(); Console.WriteLine($\u0026#34;Version:\\t{ip.Version}\u0026#34;); Console.WriteLine($\u0026#34;Length:\\t\\t{ip.TotalLength}\u0026#34;); Console.WriteLine($\u0026#34;Serial:\\t\\t{BitArrayToInt(s, 0, 15)}\u0026#34;); Console.WriteLine($\u0026#34;DF:\\t\\t{symbol[14]}\u0026#34;); Console.WriteLine($\u0026#34;MF:\\t\\t{symbol[13]}\u0026#34;); Console.WriteLine($\u0026#34;Offest:\\t\\t{BitArrayToInt(symbol, 0, 12)}\u0026#34;); Console.WriteLine($\u0026#34;From:\\t\\t{ip.SourceAddress}\u0026#34;); Console.WriteLine($\u0026#34;To:\\t\\t{ip.DestinationAddress}\u0026#34;); Console.WriteLine($\u0026#34;Protocol:\\t{ip.Protocol}\u0026#34;); for (int i = 0; i \u0026lt; ip.HeaderData.Length; i++) { if (ip.HeaderData[i] \u0026lt; 16) { Console.Write(\u0026#34;0{0:X} \u0026#34;, ip.HeaderData[i]); } else { Console.Write(\u0026#34;{0:X} \u0026#34;, ip.HeaderData[i]); } if (i % 4 == 3) { Console.WriteLine(); } } Console.WriteLine(); } 这里的e就是捕获到的包，然后就可用进行一系列的处理了\n","date":"2023-12-25T10:01:56Z","permalink":"/post/articles/2023/%E7%AE%80%E5%8D%95%E7%94%A8%E4%B8%8Bsharppcap/","title":"简单用下SharpPcap"},{"content":"问题 最近入了台笔记本，7840HS的，CPU强核显也强\n（主要是没米了）\n然后毕竟是做游戏相关的，Blender总得装上吧\n然后发现，跑Cycles渲染器会报错\nHIP binary kernel for this graphics card compute capability (11.0) not found\n就会报这个错误\n解决办法 从网站上下源码编译Blender就好\nBuild Blender On Windows\n需要安装\nVS2022的C++桌面开发工作负载 TortoiseSVN Git CMake perl python 然后就按官方文章讲的，运行make update和make就好\n问题分析 radeon780m对应得代码是gfx1103\n但是blender正常下载的版本还没有带对应这个代码的驱动文件\n在最新的代码中，已经包含了生成代号gfx1103的驱动文件的代码了\n","date":"2023-11-29T22:39:09Z","permalink":"/post/articles/2023/radeon780m%E6%A0%B8%E6%98%BE%E8%B7%91blender%E7%9B%B8%E5%85%B3%E7%9A%84%E9%97%AE%E9%A2%98/","title":"Radeon780m核显跑Blender相关的问题"},{"content":"\u0026lt;0x00\u0026gt; ListView是什么 贴张图就好了\n就是类似这种一行一行展示信息的控件\n当然也不一定是像这种的类似表格的样子(这种后面会讲)\n但都是一行一行的以行为单位的显示一些集合类的显示控件\n\u0026lt;0x01\u0026gt; 先讲WPF里面自带的ListView 先给个代码，做一个最简单的演示\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;/\u0026gt; \u0026lt;/Window\u0026gt; //ViewModel.cs //常用引用 namespace WPFTest { internal class ViewModel { public List\u0026lt;string\u0026gt; SomeItems { get; set; } = new List\u0026lt;string\u0026gt;() { \u0026#34;aaa\u0026#34;, \u0026#34;bbb\u0026#34;, \u0026#34;ccc\u0026#34;, \u0026#34;ddd\u0026#34;, \u0026#34;fff\u0026#34;, }; } } 演示的结果\n这个就是最简单的ListView，也是WPF默认的样式\n(从这期开始的图会多一点，以前的博文也会慢慢补图，感觉没图还是不好理解)\n我们可以先分析下这个控件需要什么，我们能得到什么\n首先就肯定是需要一个集合啦\n集合无所谓类型(应该都能用，看你怎么绑定)\n然后我们能得到一个这样的显示\n我们可以看到，在这个控件里面已经实现了页面滚动与子项的选择\n(可以说除了有点不符合当代审美之外都没啥问题)\n单项添加 有时候可能也不是集合提供子项，可能本身就是写死的\n这时候就要自己写ListViewItem了\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;ListView SelectionMode=\u0026#34;Single\u0026#34;\u0026gt; \u0026lt;ListViewItem Content=\u0026#34;something1\u0026#34;/\u0026gt; \u0026lt;ListViewItem Content=\u0026#34;something2\u0026#34;/\u0026gt; \u0026lt;ListViewItem Content=\u0026#34;something3\u0026#34;/\u0026gt; \u0026lt;ListViewItem Content=\u0026#34;something4\u0026#34;/\u0026gt; \u0026lt;ListViewItem Content=\u0026#34;something5\u0026#34;/\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 每个ListViewItem也都能设置自己的Background之类的属性，这里就不多写了\n美化子项 那我们还是要稍微美化一下的，虽然这样搞已经能用了\n我们有两种方式\n一个是操作ItemStyle，另一个是操作ItemTemplate\n操作ItemStyle \u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;\u0026gt; \u0026lt;ListView.ItemContainerStyle\u0026gt; \u0026lt;Style TargetType=\u0026#34;ListViewItem\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;FontSize\u0026#34; Value=\u0026#34;20\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;Foreground\u0026#34; Value=\u0026#34;Red\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;Background\u0026#34; Value=\u0026#34;BurlyWood\u0026#34;/\u0026gt; \u0026lt;Style.Triggers\u0026gt; \u0026lt;!--Trigger这里就偷懒了--\u0026gt; \u0026lt;/Style.Triggers\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;/ListView.ItemContainerStyle\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 通过ListView.ItemContainerStyle里面设置ListViewItem的样式\n样式就按一般的方式走，这里就随便设置了下，Triggers也能设置，这里懒得写了\n操作ItemTemplate 这里需要修改下ViewModel，为了更方便的绑定\n//ViewModel2.cs //常用引用 namespace WPFTest { internal class ViewModel2 { public List\u0026lt;TypicalItem\u0026gt; SomeItems { get; set; } = new List\u0026lt;TypicalItem\u0026gt;() { new TypicalItem(\u0026#34;aaa\u0026#34;, new SolidColorBrush(Color.FromRgb(82,114,248))), new TypicalItem(\u0026#34;bbb\u0026#34;, new SolidColorBrush(Color.FromRgb(72,137,217))), new TypicalItem(\u0026#34;ccc\u0026#34;, new SolidColorBrush(Color.FromRgb(91,199,240))), new TypicalItem(\u0026#34;ddd\u0026#34;, new SolidColorBrush(Color.FromRgb(72,214,217))), new TypicalItem(\u0026#34;eee\u0026#34;, new SolidColorBrush(Color.FromRgb(82,248,205))), }; } class TypicalItem { public string Name { get; set; } public Brush Color { get; set; } public TypicalItem(string name, Brush color) { Name = name; Color = color; } } } \u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel2/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;\u0026gt; \u0026lt;ListView.ItemTemplate\u0026gt; \u0026lt;DataTemplate DataType=\u0026#34;ListViewItem\u0026#34;\u0026gt; \u0026lt;Border\u0026gt; \u0026lt;TextBox Text=\u0026#34;{Binding Name}\u0026#34; Background=\u0026#34;{Binding Color}\u0026#34; FontSize=\u0026#34;20\u0026#34;/\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/DataTemplate\u0026gt; \u0026lt;/ListView.ItemTemplate\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 效果就差不多这样\n区别 实际上两个的侧重点是不一样的\nItemContainerStyle重点在每个子项的样式设置\n比方说就是每个子项的背景，子项按下的动画效果之类的\nItemTemplate侧重在子项内部是怎么显示的\n比方说写了个数据类型，里面有好几条属性\n那么就可以用ItemTemplate来自定义这些属性怎么显示\n我怀疑ListViewItem里面就是包着自己的UI树的\n但可视化树没显示，也不敢说死\n这两个的效果肯定是能合并在一起的，毕竟这俩侧重点是完全不一样的\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel2/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;\u0026gt; \u0026lt;!--其实就加了这一个块--\u0026gt; \u0026lt;ListView.ItemContainerStyle\u0026gt; \u0026lt;Style TargetType=\u0026#34;ListViewItem\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;Background\u0026#34; Value=\u0026#34;BurlyWood\u0026#34;/\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;/ListView.ItemContainerStyle\u0026gt; \u0026lt;ListView.ItemTemplate\u0026gt; \u0026lt;DataTemplate DataType=\u0026#34;ListViewItem\u0026#34;\u0026gt; \u0026lt;Border\u0026gt; \u0026lt;TextBox Text=\u0026#34;{Binding Name}\u0026#34; Background=\u0026#34;{Binding Color}\u0026#34; FontSize=\u0026#34;20\u0026#34;/\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/DataTemplate\u0026gt; \u0026lt;/ListView.ItemTemplate\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 和程序行为合并 那下一步就是写选中子项的行为了\n我们可以通过设置SelectionChanged事件\n为了演示，先修改下前端代码，这里举选择子项切换Border背景颜色的例子\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel2/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;!--加了个Grid，上下分割--\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34; SelectionMode=\u0026#34;Single\u0026#34; SelectionChanged=\u0026#34;ListView_SelectionChanged\u0026#34;\u0026gt; \u0026lt;ListView.ItemContainerStyle\u0026gt; \u0026lt;Style TargetType=\u0026#34;ListViewItem\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;Background\u0026#34; Value=\u0026#34;BurlyWood\u0026#34;/\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;/ListView.ItemContainerStyle\u0026gt; \u0026lt;ListView.ItemTemplate\u0026gt; \u0026lt;DataTemplate DataType=\u0026#34;ListViewItem\u0026#34;\u0026gt; \u0026lt;Border\u0026gt; \u0026lt;TextBox Text=\u0026#34;{Binding Name}\u0026#34; Background=\u0026#34;{Binding Color}\u0026#34; FontSize=\u0026#34;20\u0026#34;/\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/DataTemplate\u0026gt; \u0026lt;/ListView.ItemTemplate\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;!--用来显示子项切换的效果的Border--\u0026gt; \u0026lt;Border Grid.Row=\u0026#34;1\u0026#34; Name=\u0026#34;MyBorder\u0026#34;/\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Window\u0026gt; //MainWindow.xaml.cs //引用 namespace WPFTest { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e) { MyBorder.Background = (e.AddedItems[0] as TypicalItem).Color; } } } 这样就解决了\n.xaml的代码就不解释了，.cs的代码还是能讲一下的\n主要就是用e.AddedItems[0]拿到选中的TypicalItem对象\n(e.AddedItems[0]这个语法确实很怪，估计是主要是为了支持多个选中的情况)\n如果给ListView起了名字的话也可以这么写(这里起名是MyListView)\n//MainWindow.xaml.cs //引用 namespace WPFTest { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void MyListView_SelectionChanged(object sender, SelectionChangedEventArgs e) { MyBorder.Background = (MyListView.SelectedItem as TypicalItem).Color; } } } 一般来说这么访问选中的子项会多一点\n因为有些时候会去想双击的代码，这就要用MouseDoubleClick事件了\n但MouseDoubleClick是不带SelectionChangedEventArgs e的\n也就是说我们是不能靠传参变量e来获取选择了哪个子项的\n但靠MyListView.SelectedItem这种方式就方便很多，都能用了\n顺便在这里提一点，如果要定义多个鼠标事件，不能采用传统的.xaml写事件然后在.cs写\n因为这么写运行的时候这些MouseHandler会相互屏蔽而导致有些就触发不了\n比如按传统方式同时设置MouseDoubleClick和MouseRightButtonDown，只能触发前者\n要都能触发，应该在控件的初始化用代码初始化这些Handler\n.xaml里面可以按传统方法留一个，但别的都要靠代码方式添加\n//MainWindow.xaml.cs(随便举例的，与前面的代码无关) //引用 namespace WPFTest { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //就是在这里自己添加 MyListView.AddHandler(MouseDoubleClickEvent, new MouseButtonEventHandler(this.MyListView_MouseDoubleClick), true); MyListView.AddHandler(MouseRightButtonDownEvent, new MouseButtonEventHandler(this.MyListView_MouseRightButtonDown), true); } private void MyListView_MouseDoubleClick(object sender, MouseButtonEventArgs e) { //处理代码 } private void MyListView_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { //处理代码 } } } 还有就是我还没啥办法实现类似Button绑定ICommand的做法\n但其实可以把方法本体写在VM里，然后在.cs里用DataContext调命令，实现猴版的MVVM\n这里就不细讲了\n网格布局GridView 博客最上面的图就是使用了GridView的效果\n(就是这张)\nGridView是嵌套在ListView里面的，具体代码如下\n(还是用得ViewModel2.cs)\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel2/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;\u0026gt; \u0026lt;ListView.View\u0026gt; \u0026lt;GridView\u0026gt; \u0026lt;GridViewColumn Header=\u0026#34;Name\u0026#34; DisplayMemberBinding=\u0026#34;{Binding Name}\u0026#34;/\u0026gt; \u0026lt;GridViewColumn Header=\u0026#34;Color\u0026#34;\u0026gt; \u0026lt;GridViewColumn.CellTemplate\u0026gt; \u0026lt;DataTemplate DataType=\u0026#34;local:TypicalItem\u0026#34;\u0026gt; \u0026lt;TextBlock Text=\u0026#34;{Binding Color}\u0026#34; Background=\u0026#34;{Binding Color}\u0026#34;/\u0026gt; \u0026lt;/DataTemplate\u0026gt; \u0026lt;/GridViewColumn.CellTemplate\u0026gt; \u0026lt;/GridViewColumn\u0026gt; \u0026lt;/GridView\u0026gt; \u0026lt;/ListView.View\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 这样就实现了类似前面那张图的效果了\n这里不难看出GridView是隶属于ListView的\n然后对于每一个格子，也是能自定义Template的\n最上面的是Header，也能自定义\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel2/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;\u0026gt; \u0026lt;ListView.View\u0026gt; \u0026lt;GridView\u0026gt; \u0026lt;GridViewColumn Header=\u0026#34;Name\u0026#34; DisplayMemberBinding=\u0026#34;{Binding Name}\u0026#34;/\u0026gt; \u0026lt;GridViewColumn\u0026gt; \u0026lt;!--就改了这里--\u0026gt; \u0026lt;GridViewColumnHeader\u0026gt; \u0026lt;TextBlock Text=\u0026#34;emm\u0026#34; Background=\u0026#34;BurlyWood\u0026#34;/\u0026gt; \u0026lt;/GridViewColumnHeader\u0026gt; \u0026lt;GridViewColumn.CellTemplate\u0026gt; \u0026lt;DataTemplate DataType=\u0026#34;local:TypicalItem\u0026#34;\u0026gt; \u0026lt;TextBlock Text=\u0026#34;{Binding Color}\u0026#34; Background=\u0026#34;{Binding Color}\u0026#34;/\u0026gt; \u0026lt;/DataTemplate\u0026gt; \u0026lt;/GridViewColumn.CellTemplate\u0026gt; \u0026lt;/GridViewColumn\u0026gt; \u0026lt;/GridView\u0026gt; \u0026lt;/ListView.View\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 简单写一下就是这样了\n前面讲的SelectionChanged和MouseDoubleClick之类的还是写在ListView的块里\n这样就可以实现表格化的效果了\n\u0026lt;0x02\u0026gt; 讲讲ModernWPF的ListView 因为有自己的练手项目在用这个包，所以就顺便讲讲了\n(Nuget上有好多叫ModernWPF的包，我自己用的是这个)\n之前的博客也有讲过一点\n在ModernWPF的包里面有两种ListView实现\n一种是使用WPF原生的ListView但修改了Style来匹配风格\n另一种是包里面自己写的ListView\n前者就要求别去改Style，不然所有的样式又要自己写\n后者的化代码风格会更像UWP开发，有些写法确实挺新的\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ... xmlns:ui=\u0026#34;http://schemas.modernwpf.com/2019\u0026#34; ui:WindowHelper.UseModernWindowStyle=\u0026#34;True\u0026#34;\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;!--这里用的是最前面的ViewModel--\u0026gt; \u0026lt;local:ViewModel/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;ListView Grid.Row=\u0026#34;0\u0026#34; ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;/\u0026gt; \u0026lt;ui:ListView Grid.Row=\u0026#34;1\u0026#34; ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;/\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Window\u0026gt; 这里演示了这个包里面的两种写法，效果上是一样的\n其余大部分都可以按WPF原生的写法写\n但如果要用GridView，那么只有原生的写法(这个包没自己写)\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ... xmlns:ui=\u0026#34;http://schemas.modernwpf.com/2019\u0026#34; ui:WindowHelper.UseModernWindowStyle=\u0026#34;True\u0026#34;\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel2/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;\u0026gt; \u0026lt;ListView.View\u0026gt; \u0026lt;GridView\u0026gt; \u0026lt;GridViewColumn Header=\u0026#34;Name\u0026#34; DisplayMemberBinding=\u0026#34;{Binding Name}\u0026#34;/\u0026gt; \u0026lt;GridViewColumn Header=\u0026#34;Color\u0026#34; DisplayMemberBinding=\u0026#34;{Binding Color}\u0026#34;/\u0026gt; \u0026lt;/GridView\u0026gt; \u0026lt;/ListView.View\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 这里可以看得，如果用GridView的话，右边的滚动条时不会收纳的\n(准确来说自动从收纳状态变成展开状态)\n别的自定义之类的也和WPF原生写法一样，就是尽量别动Style\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window ... xmlns:ui=\u0026#34;http://schemas.modernwpf.com/2019\u0026#34; ui:WindowHelper.UseModernWindowStyle=\u0026#34;True\u0026#34;\u0026gt; \u0026lt;Window.DataContext\u0026gt; \u0026lt;local:ViewModel2/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; \u0026lt;ListView ItemsSource=\u0026#34;{Binding SomeItems}\u0026#34;\u0026gt; \u0026lt;ListView.View\u0026gt; \u0026lt;GridView\u0026gt; \u0026lt;GridViewColumn Header=\u0026#34;Name\u0026#34; DisplayMemberBinding=\u0026#34;{Binding Name}\u0026#34;/\u0026gt; \u0026lt;GridViewColumn Header=\u0026#34;Color\u0026#34;\u0026gt; \u0026lt;GridViewColumn.CellTemplate\u0026gt; \u0026lt;DataTemplate DataType=\u0026#34;local:TypicalItem\u0026#34;\u0026gt; \u0026lt;Button Content=\u0026#34;{Binding Color}\u0026#34; Background=\u0026#34;{Binding Color}\u0026#34;/\u0026gt; \u0026lt;/DataTemplate\u0026gt; \u0026lt;/GridViewColumn.CellTemplate\u0026gt; \u0026lt;/GridViewColumn\u0026gt; \u0026lt;/GridView\u0026gt; \u0026lt;/ListView.View\u0026gt; \u0026lt;/ListView\u0026gt; \u0026lt;/Window\u0026gt; 写这篇博客真的花了不少时间，大家看得开心就好\n","date":"2023-09-18T20:10:22Z","permalink":"/post/categories/wpf/wpf%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B006-%E5%85%B3%E4%BA%8Elistview/","title":"WPF学习笔记06-关于ListView"},{"content":"前言 我在半年前已经将我的IPad出手了，所以这里写的体验都是基于半年前的IPadOS16.1\n为什么半年后还写这个呢，主要是想练练手，没怎么写过评测类的文章，顺便分享下我的体验\n\u0026lt;0x01\u0026gt; 为什么选择IPad 这个小标题看着有些怪哈，但既然说放弃，那肯定有一开始选择的理由的\n好不容易考上了大学，总该为自己添置些设备\n当时的用的电脑还是老笔记本，但性能还够用，就先不换了\n但高中的时候啊，我发觉书和卷子实在是太多了\n平时就是一大堆书和卷子垒在桌子上，找书找卷子非常困难\n所以考上大学我就想买个平板，来做我的移动图书馆\n当时我的选择有IPad系列，小米平板5系列，OPPOPad系列还有就是二手板子\n当时看了好多评测，图稳选择了IPad系列\n因为它软件丰富笔好用，硬件强悍屏准确(IPad数字系列除外)\n\u0026lt;0x02\u0026gt; 错误的选择，为不存在的需求买单 最开始我只是想买台IPad记笔记\n然后不断的增加可能的需求\n画画，开发，剪视频，修图还有打原神😅\n也没怎么考虑未来真的会不会干这些事情\n反正从原来打算收一台IPadAir5一路升级到买一台全新的IPadPro2022-1T\n还买了支笔(笔二手的，大概五百多)\n现在看来就挺离谱的，实际上我根本没有上述的刚需\n(画画有别的选择，开发用IPad基本没戏，剪视频修图更是不会用IPad，也就打原神了)\n但目前来看，有这样一台性能强劲的IPad，好好用个五六年也还好\n为啥最后放弃使用IPad呢？\n\u0026lt;0x03\u0026gt; IPadOS，使我放弃的根本 到手后，我也是第一时间开始熟悉IPadOS\n我对IPad还是挺熟悉的，小学的时候用过IPadMini2(拆机尸体还在我架子上)\n激活后没一会儿，差不多就熟悉了\n买了Procrate、Goodnote5，差点也买了LumaFusion(还好没买)\n(但我现在还是吹爆Goodnote5，这是我用过最好用的笔记软件，体验非常好)\n装上苹果的库乐队，玩了好一会\n最后当然是装上原神，体验很流畅，就是掉电比我想象得快\n(原Bench果然名不虚传)\n正式进入了大学，这台IPad也陪伴了我一个学期，我也开始觉得这IPad并不能满足我需求\n正如苹果宣传的一样，\u0026quot;你的下一台电脑，何必是电脑\u0026quot;，我希望IPad能有电脑的能力\n也不是说有多强的性能，能码字，能写代码，满足外出工作需求就够了\n尤其是自从IPad搭载m1以来，一直有传言说IPad会搭载MacOS，我也一直有这不切实际的幻想吧\n(说真的，如果IPad能搭载MacOS，直接绝杀，所有轻薄本都不用混了，可惜不可能)\n于是我发现，在着上面搞开发几乎不可能，而我又是计算机系的学生\n(我知道可以搭CodeServer，但这样不是任何设备都能搞开发吗)\n本来想通过些别的办法解决，比方说UTM虚拟机跑Windows\n(人家开发者真的猛，都搞定了m1的GPU驱动)\n结果呢，发现完美解决要越狱，打开JIT加速才有比较好的性能\n而我手头的IPad本身系统版本就新，我还手贱升级到IPadOS16，莫得退路\n其实不越狱也能跑，就是靠开发者签名跑JIT加速，然后再跑UTM\n这个方法确实能跑，但要么你自己是开发者，要么找别的开发者签名\n前者要苹果开发者年费700，后者感觉不是很稳，所以这条子路基本也寄了\n到目前为止，不能开发就不能开发吧，那就做做生产力吧\n出门干活的话用远程桌面续续命好了，反正我基本不出门\n然后就是我最头疼的问题，苹果的文件管理\n这个倒是购买前我没去思考的问题，都什么年代了，总该有个合理的文件管理吧\n但IPadOS就是没有，那个叫文件的应用并不是给你管理文件的，顶多说让你把IPad当硬盘用\n或者说给不同的软件做中转用的\n比方说编辑个文档，我先要在软件里写好，然后到处到文件里，再传文件，有点麻烦\n(也赖我，不用MacOS)\n而且我常用Windows，和IPad交换文件极其困难\n效率最高的方式居然是拿U盘拷文件过去\n(可以用KDE Connect来传文件，当时不知道这软件有IOS版)\n忍了一学期，最后实在是忍不了一点，最后就放弃IPad了\n基本上不是IPad本身的问题，只能说IPadOS不适合我\nIPadOS很喜欢教用户做事，苹果嘛，老毛病了\n但IPad上的软件也喜欢教用户做事，基本上可设置选项不多，让用户自己适应\n这也算我放弃IPad的一个原因\n\u0026lt;0x04\u0026gt; IPad是个好工具，但它不适合我 说了这么多，其实我仍然觉得这款IPadPro是一款不错的产品\n优秀的工业设计，顶级的屏幕，强劲的性能，丰富的软件，还有优秀的系统体验\n我是真的很喜欢IPadPro的机身，全铝磨砂机身，摸着很舒适\n屏幕是我见过最好的屏幕，看HDR视频简直就是享受，搭配上四扬声器，看电影很棒\nm1，不必说，移动端数一数二的存在\n\u0026quot;Only Apple can do\u0026quot;的软件生态，几乎所有的创意工作都能在这上面完成(除了开发)\nIPadOS虽然没有能用的文件管理，但整个系统体现的就是灵动，按下就有反馈\n而且说实在的，IPadPro的价格也没有很贵(如果能发挥它的实力的话)\n像画画领域，IPadPro+ApplePencil的价格跟Wacom高端数位屏比，苹果这套就有了性价比\n影视领域，现在有了DavinciForIPad，白天拍，路上就能粗剪，这带来的效率提升是巨大的\n修图的话，用那些软件，差不多拍完回去的路上就能完成调色，非常优雅(这个也不一定要Pro的)\n搞音乐更不用说，库乐队已经很强大了，现在还有LogicForIPad，更强大\n而且貌似IPad上还有些碟机软件，接上碟机就能用，比macbook更轻便\n举这些例子也不是说鼓励大家买IPad，我也只是挑IPad的专长讲，具体我也不是很熟\n真搞生产力，IPad的弱势也有，比方说程序开发\n虽然说有在搞XcodeForIPad，但也不用抱太大希望，这个要看你的开发贴不贴近苹果生态\n(总不能指望这玩意能编译C#之类吧)\n搞CodeServer的方案我总是感觉很野鸡，毕竟这样的话啥设备不能开发啊\n还有就是，我原以为，IPad是最脱离苹果生态的产品，哪怕没有一个苹果的产品也能用得好\n但我现在觉得最脱离苹果生态的产品还得是macbook，IPad更像是macbook的附属品\n我有个室友，他的日用电脑就是macbook，在我的IPad出手前借给他用了几天\n他整体体验下来感觉非常妙，随航，隔空投递，接力真的很舒服\n尤其是随航，它可以让IPad成为macbook的副屏，我们不清楚苹果是这么实现的，这玩意延迟巨低\n所以，IPad好吗，它真的很好，但它不适合我\n(所以我现在换成了Samsung的TabS7😋)\n","date":"2023-09-06T15:21:11Z","permalink":"/post/articles/2023/%E6%88%91%E4%B8%BA%E4%BB%80%E4%B9%88%E6%94%BE%E5%BC%83%E4%BD%BF%E7%94%A8ipad/","title":"我为什么放弃使用IPad"},{"content":"\u0026lt;0x00\u0026gt; 介绍下ModernWPF 这个其实是个Nuget包，里面有很多现代化设计的WPF组件\n不是之前说嘛，在WPF里面做现代化的UI设计很复杂\n现在差不多就是别人已经把最复杂的实现写完了，我们只要调用就好了\n这个包的实现风格是UWP风格(也就是WinUI2)\n里面的很多用法是参考UWP的\n所以有些组件的用法跟WPF的常见实现不一样(就比如这里讲的导航界面)\n安装的话用VS的Nuget包管理器就可以\n(Nuget上有很多包叫ModernWPF，我用的是这个，别的实现我就不清楚了，最好一样吧)\nGithub项目主页\n在Github上也有这个项目的Wiki，所有组件的使用都有列举一点\n因为差不多是完全复刻UWP的，所以有些样例会直接跳转到微软的文档\n照样看就是了，代码实现是差不多的(跳转的文档是英文的，最好就直接看英文，这样最准确)\n\u0026lt;0x01\u0026gt; 如果是一般的WPF项目，我们该怎么实现导航 还是先稍微讲讲一般的WPF项目里怎么实现导航\n假设我们有MainWindow.xaml，PageViewModel.cs，SomePage.xaml\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;window ...\u0026gt; \u0026lt;window.DataContent\u0026gt; \u0026lt;local:PageViewModel/\u0026gt; \u0026lt;/Window.DataContent\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;Grid.ColumnDefinitions\u0026gt; \u0026lt;!--第一列放导航的按钮--\u0026gt; \u0026lt;ColumnDefinition Width=\u0026#34;100\u0026#34;/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;/Grid.ColumnDefinitions\u0026gt; \u0026lt;StackPanel\u0026gt; \u0026lt;!--若干按钮，具体就不设计了--\u0026gt; \u0026lt;Button Command=\u0026#34;{Binding NavigateToPage}\u0026#34;/\u0026gt; \u0026lt;/StackPanel\u0026gt; \u0026lt;!--内容展示的页面--\u0026gt; \u0026lt;UserControl Grid.Column=\u0026#34;1\u0026#34; Content=\u0026#34;{Binding CurrentPage}\u0026#34;/\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/window\u0026gt; //PageViewModel.cs //常见的引用 namespace TestProject.ViewModel { internal partial class PageViewModel:ObservableObject { [ObservableProperty] public object? currentPage; public ICommand NavigateToPage{get;set;} public PageViewModel() { CurrentPage=new SomePage(); NavigateToPage=new RelayCommand(()=\u0026gt;CurrentPage=new SomePage()); } } } (这里的一些没见过的东西是在Community.MVVM包里面的，上一篇结尾有介绍)\n\u0026lt;!--SomePage.xaml--\u0026gt; \u0026lt;UserControl ...\u0026gt; \u0026lt;TextBlock Text=\u0026#34;Emmm\u0026#34; Foreground=\u0026#34;White\u0026#34; HorizontalAlignment=\u0026#34;Center\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34; FontSize=\u0026#34;32\u0026#34;/\u0026gt; \u0026lt;/UserControl\u0026gt; 这里通过UseControl控件来存放我们自己写的SomePage\n导航栏的每个按钮会绑定一个Command，来控制UserControl的Content\n因为这个Content就存放具体的页面嘛，所以只要改这个Content就相当于切换页面了\n之后有什么多的页面就是先新建一个UserControl控件并完成设计\n然后再在PageViewModel.cs里面实现切换代码(就是写一行改CurrentPage的代码)\n最后在MainWindow.xaml里面为对应按钮绑定Command就好\n其实最好是在PageViewModel里用一个object数组存储不同的page，我这里就偷懒了\n还有就是导航栏我也没咋设计，基本上就是用Triger配合Setter做就可以了，这里也摸了\n\u0026lt;0x02\u0026gt; 用ModernWPF里面的NavigationView实现导航 (虽然是介绍怎么在ModernWPF里面实现导航，因为用法极像UWP，也算介绍UWP的开发了)\n在ModernWPF中用NavigationView做导航就没有类似按钮绑定Command的用法了\n准确来说，用ModernWPF甚至都不需要写一个对应的VM\n那么怎么切换页面呢\n差不多就是靠.xaml附带的.cs里面实现了\n先上代码\n(ModernWPF有使用前的一些步骤，在它的readme有写，就两步，我就不介绍了)\n\u0026lt;!--ModernUITest.xaml--\u0026gt; \u0026lt;Window x:Class=\u0026#34;TestProject.ModernUITest\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; xmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; xmlns:ui=\u0026#34;http://schemas.modernwpf.com/2019\u0026#34; xmlns:local=\u0026#34;clr-namespace:TestProject\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; ui:WindowHelper.UseModernWindowStyle=\u0026#34;True\u0026#34; Title=\u0026#34;ModernUITest\u0026#34; Height=\u0026#34;800\u0026#34; Width=\u0026#34;800\u0026#34;\u0026gt; \u0026lt;ui:NavigationView IsBackButtonVisible=\u0026#34;Collapsed\u0026#34; IsTitleBarAutoPaddingEnabled=\u0026#34;False\u0026#34; PaneTitle=\u0026#34;Test\u0026#34; PaneDisplayMode=\u0026#34;Auto\u0026#34; ItemInvoked=\u0026#34;NavigationView_ItemInvoked\u0026#34;\u0026gt; \u0026lt;ui:NavigationView.MenuItems\u0026gt; \u0026lt;ui:NavigationViewItem Icon=\u0026#34;Home\u0026#34; Tag=\u0026#34;TestProject.Page.PageTest1\u0026#34; Content=\u0026#34;Sample Item 1\u0026#34; IsSelected=\u0026#34;True\u0026#34;/\u0026gt; \u0026lt;ui:NavigationViewItem Icon=\u0026#34;Keyboard\u0026#34; Content=\u0026#34;Sample Item 2\u0026#34; Tag=\u0026#34;TestProject.Page.PageTest2\u0026#34;/\u0026gt; \u0026lt;/ui:NavigationView.MenuItems\u0026gt; \u0026lt;UserControl Name=\u0026#34;UC\u0026#34;/\u0026gt; \u0026lt;/ui:NavigationView\u0026gt; \u0026lt;/Window\u0026gt; //ModernUITest.xaml.cs //常见的引用 namespace TestProject { public partial class ModernUITest : Window { public ModernUITest() { InitializeComponent(); UC.Content = new PageTest1();//给一个默认的页面 } private void NavigationView_ItemInvoked(ModernWpf.Controls.NavigationView sender, ModernWpf.Controls.NavigationViewItemInvokedEventArgs args) { if(args.IsSettingsInvoked==true) { NavigationView_Navigate(typeof(int), args.RecommendedNavigationTransitionInfo); //我这里没写Setting的page，所以就随便写了typeof(int) } else if(args.InvokedItemContainer!=null) { NavigationView_Navigate(Type.GetType(args.InvokedItemContainer.Tag.ToString()), args.RecommendedNavigationTransitionInfo); } } private void NavigationView_Navigate(Type navPageType, NavigationTransitionInfo transitionInfo) { Type preNavPageType = UC.Content.GetType(); if(navPageType is not null \u0026amp;\u0026amp; !Type.Equals(navPageType, preNavPageType)) { if(navPageType==typeof(PageTest1)) { UC.Content = new PageTest1(); } if (navPageType == typeof(PageTest2)) { UC.Content = new PageTest2(); } } } } } (PageTest1和PageTest2的代码就不贴了，就纯纯的一行字)\n先讲下包里的东西 ui:WindowHelper.UseModernWindowStyle=true表示使用ModernWPF的窗口样式\nwindow控件下面只有ui:NavigationView\n先在ui:NavigationView里面设置ui:NavigationView.MenuItems\n再在里面设置ui:NavigationViewItem，要几个就设置几个\nui:NavigationView下面有个UserControl，这个就是我们要切换的页面\nui:NavigationView里面有些属性\nIsBackButtonVisible=\u0026quot;Collapsed\u0026quot;是关闭默认的返回按钮\nIsTitleBarAutoPaddingEnabled=\u0026quot;False\u0026quot;是关闭顶栏的自动排布\nPaneTitle=\u0026quot;Test\u0026quot;就是会显示在最上面的那行字，展开时会显示\nPaneDisplayMode=\u0026quot;Auto\u0026quot;这里显示了自动展开，就是最大化窗口时会自动展开\nItemInvoked=\u0026quot;NavigationView_ItemInvoked\u0026quot;这个是实现导航最关键的部分，后面讲\nui:NavigationViewItem里别的应该都能看懂\n就是Tag这个是最关键的，规范的话里面要填上要导航页面对应的类型全名\n就是对应页面的namespace.类名，因为按规范导航会用到Type来判断\n(估计是因为微软文档写的是用Frame实现页面导航)\n.xaml里要干的事 在ui:NavigationView里写上ItemInvoked=\u0026quot;NavigationView_ItemInvoked\u0026quot;，名字随意\n在ui:NavigationViewItem里正确写上Tag\n给UserControl起个名，好让我们在.cs拿到对象\n剩下的事就交给.cs了\n.cs里要干的事 按上面的样式写就可以了\n大致的调用过程 在NavigationView里面切换页面会触发ItemInvoked绑定的事件\n在ItemInvoked绑定的方法中，先判定是不是要切换到Setting的页面\n(args.IsSettingsInvoked==true) (因为这里的NavigationView没有设置IsSettingsVisible=\u0026quot;False\u0026quot;)\n然后判定args.InvokedItemContainer!=null\n这两条路径都会调用导航方法NavigationView_Navigate\n这个导航方法会要求传入一个Type和一个NavigationTransitionInfo\n(NavigationTransitionInfo在这里确实没啥用)\n这个Type用来判断具体要切换到哪个页面\n其实Tag随便写也没啥问题，主要看写的导航方法是什么\n这里我写的代码极致精简的话完全可以初始化一个Dictionary\u0026lt;string,object\u0026gt;\n然后直接在ItemInvoked绑定的方法中切换就可以了\n像这样\nnamespace TestProject { public partial class ModernUITest : Window { Dictionary\u0026lt;string, object\u0026gt; pages; public ModernUITest() { InitializeComponent(); pages = new Dictionary\u0026lt;string, object\u0026gt; { {\u0026#34;Page1\u0026#34;,new PageTest1()}, {\u0026#34;Page2\u0026#34;,new PageTest2()}, {\u0026#34;Setting\u0026#34;,new SettingPage()}, }; UC.Content = pages[\u0026#34;Page1\u0026#34;]; } private void NavigationView_ItemInvoked(ModernWpf.Controls.NavigationView sender, ModernWpf.Controls.NavigationViewItemInvokedEventArgs args) { if (args.IsSettingsInvoked == true) { UC.Content = pages[\u0026#34;Setting\u0026#34;]; } else if (args.InvokedItemContainer != null) { UC.Content = pages[args.InvokedItemContainer.Tag.ToString()]; } } } } 对应的Tag再改改就好\n\u0026lt;0x03\u0026gt; 两种做法的区别 我个人来看的话，按第一种实现会更加优雅\n毕竟前端只要有按钮绑定Command就好了\n但这么做的话就是控件要自己开发了(也确实不是大问题，主要是WPF现在只有维护了)\n第二种方式相对来说没那么优雅，但真正做到了前端的事前端干\n按照MVVM的理论，VM是用来连接前后端的\n但第一种方法我们仅仅是为了前端切换页面就写了个PageViewModel实现\n某种程度上确实不大符合MVVM的定义\n(我知道有别的方式可以不单独写VM，但别的方式确实没单独写VM直观)\n而第二种方法把前端的事情聚合在一起，那些新的页面都是在前端new出来的\n有啥好处呢，主要还是方便管理吧，功能更加聚合了\n","date":"2023-08-30T13:28:48Z","permalink":"/post/categories/wpf/wpf%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B005-%E6%80%8E%E4%B9%88%E7%94%A8modernwpf%E5%AE%9E%E7%8E%B0%E5%B8%A6%E5%AF%BC%E8%88%AA%E7%9A%84%E7%95%8C%E9%9D%A2/","title":"WPF学习笔记05-怎么用ModernWPF实现带导航的界面"},{"content":"\u0026lt;0x00\u0026gt; 相对更正规的MVVM的写法 前一篇文章不是讲MVVM模型嘛，然后这几天看了看别人写的代码\n确实有很多值得学习的地方\n单独拆个BaseViewModel出来 之前讲到ViewModel要实现接口INotifyPropertyChanged来通知前端属性更改\n基本上所有的VM都要用到的，所以最好就是有个BaseViewModel类，给所有的VM继承\n具体代码如下\nnamespace WPFTest.VM { public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; internal void NotifyPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } } } 其实就是把那些代码单独拆出来了\n然后所有的VM继承这个类就好了\n之前也讲到RelayCommand类也是经常用到的工具类\n所以一般这两个类的文件会放在同一个文件夹下\n这里的命名空间也进行了规范(我这里为了简单就只写缩写了)\n反正就是M/V/VM分三块地就行了\n具体还是按着规范怎么舒服怎么来\n另一种更舒适的方式添加DataContext 之前在前端引入VM的上下文是靠对应.cs文件里加this.DataContext=this;实现\n现在有相对舒适的做法\n首先要在.xaml的xmlns中引入命名空间\n然后加入具体控件的DataContext\n具体要添加的代码如下\n\u0026lt;Window ... xmlns:vm=\u0026#34;clr-namespace:WPFTest.VM\u0026#34; ...\u0026gt; ... \u0026lt;Window.DataContext\u0026gt; \u0026lt;vm:ViewModel/\u0026gt; \u0026lt;/Window.DataContext\u0026gt; ... \u0026lt;/Window\u0026gt; 这里的vm可以随便改的，ViewModel是具体的VM\n这样写的话就尽可能避开对应的.cs，尽可能把前端实现交给.xaml\n\u0026lt;0x01\u0026gt; 关于窗口透明化 之前是在Window标签里设置AllowTransparency=True和Background=\u0026quot;Transparent\u0026quot;来实现\n但这样实现的窗口性能会受很大影响(相关分析可以看大佬lindexi的博客)\n简单来说就是这样写会相对频繁的申请内存，让CPU和内存的负荷加大\n所以怎么办呢，我们也可以通过WindowChrome实现\n更多的信息可以看大佬walterlv的博客\n还有大佬丑萌气质狗的博客\n我这里就接着我的代码改改了\n具体修改的代码如下\n\u0026lt;!--前一个是隐藏window默认控件的，后一个是仅允许最小化的意思--\u0026gt; \u0026lt;!--具体也不清楚为啥，后一个必须是CanMinimize或NoResize才有透明--\u0026gt; \u0026lt;Window ... WindowStyle=\u0026#34;None\u0026#34; ResizeMode=\u0026#34;CanMinimize\u0026#34; ...\u0026gt; \u0026lt;!--修改WindowChrome--\u0026gt; \u0026lt;WindowChrome.WindowChrome\u0026gt; \u0026lt;WindowChrome GlassFrameThickness=\u0026#34;-1\u0026#34;/\u0026gt; \u0026lt;/WindowChrome.WindowChrome\u0026gt; \u0026lt;!--别的代码--\u0026gt; \u0026lt;!--在Template里设置背景透明，一定别忘加ContentPresenter--\u0026gt; \u0026lt;Window.Template\u0026gt; \u0026lt;ControlTemplate TargetType=\u0026#34;Window\u0026#34;\u0026gt; \u0026lt;Border Background=\u0026#34;Transparent\u0026#34;\u0026gt; \u0026lt;Border CornerRadius=\u0026#34;10\u0026#34; Margin=\u0026#34;10\u0026#34;\u0026gt; \u0026lt;Border.Background\u0026gt; \u0026lt;LinearGradientBrush StartPoint=\u0026#34;0,0\u0026#34; EndPoint=\u0026#34;0,1\u0026#34;\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#392669\u0026#34; Offset=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#46204F\u0026#34; Offset=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;/LinearGradientBrush\u0026gt; \u0026lt;/Border.Background\u0026gt; \u0026lt;Border.Effect\u0026gt; \u0026lt;DropShadowEffect Color=\u0026#34;Gray\u0026#34; ShadowDepth=\u0026#34;0\u0026#34; BlurRadius=\u0026#34;10\u0026#34; Opacity=\u0026#34;.5\u0026#34; Direction=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;/Border.Effect\u0026gt; \u0026lt;ContentPresenter ClipToBounds=\u0026#34;True\u0026#34;/\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Window.Template\u0026gt; \u0026lt;!--别的控件--\u0026gt; \u0026lt;/Window\u0026gt; 还有我们原来负责最小化和关闭程序的两个按钮要加上WindowChrome.IsHitTestVisibleInChrome=\u0026quot;True\u0026quot;\n这个原理在丑萌气质狗的博客里有讲\n(其实建议不搞异型窗口，容易出很多莫名其妙的问题)\n\u0026lt;0x02\u0026gt; 一些常用的Nuget包 WPF的强大还体现在有很多Nuget包\n这些包大多是些UI组件，使WPF也可以方便的实现现代化的UI\n我自己在研究的有ModernWPF和FluentWPF\nMordernWPF提供了些现代化的窗口和组件\nFluentWPF实现了Fluent设计的UI，看着好看很多\n这两个用得应该还算广吧(我在很多的WPF项目里看到有用的)\n性能方面还没怎么试(没研究明白)\n还有一个不是专门给WPF用的，而是方便MVVM设计的\n叫CommunityToolkit.Mvvm，很好用的\n平时我们写MVVM模式总是要像最上面一样写一个BaseViewModel还有一个RelayCommand\n这些常用的基础类已经在这个包里写好了\n尤其是这玩意8.0版本加入了源代码生成器的特性，现在写属性和前端通知就很方便\n具体可以看B站up-十月的寒流的视频\n(人家是真大佬，他的视频做的都很不错的)\n(所以我的博客就摸了😋)\n","date":"2023-08-23T00:49:54Z","permalink":"/post/categories/wpf/wpf%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B004-mvvm%E5%92%8C%E5%88%AB%E7%9A%84%E4%B8%80%E4%BA%9B%E4%B8%9C%E8%A5%BF%E7%9A%84%E8%A1%A5%E5%85%85/","title":"WPF学习笔记04-MVVM和别的一些东西的补充"},{"content":"\u0026lt;0x00\u0026gt; 经典在前面写点啥 很早就想去这种大型的THO活动了\n刚上大学的时候本来是想能赶上浙江的THO\n结果呢，刚好跟我们第一次期中考撞上了\n这期中考呢，还有点重要，跟分班有关系\n所以就没敢去浙T\n(顺带一提，23年浙T在10.28，我必有时间去)\n所以嘛，在B站上刚看到上海THO，一下子就买了票\n好嘛，总算是能开开眼见识下国内东方众的热情了\n本来还想看看今年的幻奏的，但看了看时间，看了看上海的住宿价格，就没去(挺可惜的)\n于是就守着火车最早的发售点买了票\n住宿就在学校寝室了，极致省钱\n最有意思的是我以为我们校车万群友也会去不少，所以我也没在群里说\n结果在开展会前两天问了两圈，没个群友去\n哇，那怕不是我一个人在展会晃悠\n我老社恐了，但还是想，票都买了，还是去吧\n\u0026lt;0x01\u0026gt; 大概讲下看展的一天(流水账警告) 我先是从学校寝室出发赶到杭州站\n最离谱的是出发前一个晚上我给手机充电，然后不知怎么晚上停电了(晚上热得睡不着)\n总共就睡了3小时不到，然后带着63%电量的手机出门\n好在说坐上列车有插头，但快充头会掉，所以就只能用边上的USB口充，好歹能充吧\n一路上充到85%，算是能熬过一天了\n下了列车，看到有人cos小玲，我就直接跟他们去了😂\n不过有一说一啊，上海15号线的指引确实比较神秘，其实出南站往一号口进去就可以了\n但上面的标志没换，我问了下老同学，他说这条线比较新，可能指引还没跟上\n一路上我社恐嘛，不敢搭话\n到红宝石路后，很明显有一群人往某个方向走，那这也很明显是去上T的嘛\n路上呢，碰见俩人貌似有点找不到路，我就鼓起勇气去搭了下话(所以有时我也在想我到底是不是社恐)\n一聊起来就马上熟悉起来了\n排队入场的时候就一直聊，他们是上海当地的，还在上高中\n甚至他们还有个同学就在场子里设摊买专辑，叫Talkie Dance，真挺厉害的\n人是真的多，当然人多才热闹\n为了倒时候能先吃饭，在他们俩拜访完他们同学后我们就在场馆内的麦那里排队了\n那边是先订餐，然后会给一张凭证，到时候来取\n(来吃麦主要是为了那个挂件)\n麦的边上是红魔馆补给站，卖些蛋糕面包，价格也挺好的\n总之我们十点入场，然后排队到十二点才吃完饭\n吃完饭后，我们仨打算先去预约园神，然后就跑到那个摊\n一扫码，哇，人是真的太多了，根本扫不出来，我们就说之后再来吧\n那干啥呢？之前就光顾着吃饭了，还没完整逛过整个场馆\n场馆说大不大，说小不小，但容纳600越共还是略显拥挤了\n那也得挤着逛啊，我们从游戏区开始，绕S型路线，逛到主舞台\n(顺便说下，一路上也看到很多抽象玩意)\n(比方说距离阿求诞辰30周年还有约354天之类的，很乐)\n好，总体有个概念了\n再按S型路线绕一圈，一路上我们仨各自爆了点米\n我入了套三月精和kk做的扑克，他们也入了点东西\n到了大概一点左右，槐南茶馆的相声节目就快开始了，我们也就赶到主舞台，找块地儿先坐\n槐南茶馆的人气确实旺啊，还没正式开始呢，舞台前就坐了一大圈人\n到了正式开始的时候，那熟悉的仨人上台了\n一开口，氦，就是个地道(手动单臂大回旋)\n具体可以看看B站的视频\n好久没有这么过瘾地听一场相声了，在这感谢槐南茶馆的付出\n(真看几次笑几次，真的就是人们喜欢的艺术啊)\n看完后啊，我的口音也给他们同步了😝\n那看完相声也下午两点了，距离展会结束还有3小时\n那咋整啊，接着逛嘛\n基本上就是绕着摊转了一圈又一圈，我们仨呢也爆了几轮米\n买了各种奇怪的东西，可谓收获颇丰\n最重要的是我们终于预约上了园神，哇，太好了\n(工作人员也跟着我们激动😄)\n最后啊，逛到下午四点，真的逛不动了(那时候已经走了一万六千步了)\n(本来还想去雾雨咖啡店打个卡，但真走不动路了)\n那就回去了，我们仨告别，各回各家\n但问题是我的高铁票是晚上七点的，也行，就在火车站歇下脚了\n上了高铁，发现后面就坐俩上T回来的人，寒暄了两句\n到了杭州，坐地铁，回寝室，睡觉\n\u0026lt;0x02\u0026gt; 一些感想 看得出来还是有不少人热爱着这个圈子，新人老人都有\n说实在的，我其实对二次元相关的了解不深\n看过的番剧屈指可数，反正就是个臭打游戏而且也没打多好的人\n但我入东方的圈子少说也有四五年了，是什么让我能一直待在这个圈子呢\n我觉得是整个氛围\n整个东方的世界观并不是由某个主题创造的，而是由大家共同创建的\n这就很好啊，感觉就跟开源的魅力一样，大家都可以有所作为\n整个圈子为大家搭建了一个舞台，谁都可以表演自己的一技之长\n这个圈子真挺不错的，也难怪整个圈子活了近20年了\n虽然圈子大了也啥人都有，但这么多年了，也算是啥风雨都经历过了吧\n热爱真的可以激发人的斗志\n像我找的那俩上海本地人的那个设摊的同学，我是真的佩服\n当然更多的也是羡慕，才高中就有些建树\n我从小也一直想干点啥，小学开始就想自己做一款游戏\n于是小学开始接触了一点点的RPG Maker\n(那时候还是用Ruby做脚本语言)\n当然，也没学进去多少\n初中在看Java，高中没时间，也只能折腾折腾计算器了\n当然，我现在也还是想着自己做一款游戏\n但相比那些年纪青青就有这么多成就的人总感觉我这个年纪能干成是应该的\n氦，或许跟教育也有关系吧，我是小县城出来的人，大家都是传统的人\n父辈还是希望我走一条传统的路，向上爬吧\n所以现在我也慢慢地承认各种平庸\n包括我自己的平庸，家庭的平庸，小县城的平庸\n(我也不是说我家庭不够好，小县城不够好之类的，仅仅是一些观念上)\n那些年纪青青有所建树的人不仅仅是他们自己很有想法\n而且他们身边的人也会帮助他们实现理想\n(貌似讲得太偏了)\n总之，这次上海THO的行程真的很不错，也算我难得会记忆的一天\n(我一般记性很差的)\n以后有THO我估计也会常去，人还是喜欢热闹点的地方\n","date":"2023-08-21T11:05:18Z","permalink":"/post/articles/2023/%E4%B8%8At%E7%9C%9F%E7%9A%84%E5%A4%AA%E5%A5%BD%E4%BA%86/","title":"上T真的太好了"},{"content":"前言 突发奇想，想研究下IL语言\n本来以为会挺难的，结果发现貌似还行\n注意：本博客不打算详细讲解IL的原理之类的\n只是大概讲讲我研究的成果\n这里推荐两篇博客，他们那讲的更详细\n一个是来自Zery的IL指令详细\n另一个是布鲁克石的30分钟？不需要，轻松读懂IL\n\u0026lt;0x01\u0026gt; 什么是IL语言 这就要从.net的运行结构开始讲起\n.net框架下面有好多们语言，但.net的运行时就一个\n所以这个运行时怎么能同时跑这么多的语言，是每个语言一个实现吗？\n肯定不是，这太麻烦了\n所以.net编译器会将它托管的语言先编译成一个中间语言，这个中间语言就是IL\n.net运行时会动态解释这个IL文件(这个跟Java差不多的流程)\n通过IL语言，.net实现了多语言共用一个运行时，也让不同语言能非常方便地整合进一个项目\n(反正编译出来的东西都是一样的)\n这个具体的过程可以看本叫《CLR via C#》(俗称C#圣经😂，最近在看)\n里面讲到过\n\u0026lt;0x02\u0026gt; 怎么查看IL语言 第一种方法，ILDasm\nVisual Studio自带的反汇编软件(但我没找到)\n参考微软的文档就好\n第二种方法，sharplab.io\n这是个给C#用的实时转换成IL的网页工具\n这个还可以转换成编译器优化后的代码甚至是给CLR执行的汇编代码\n在线执行也是可以的，而且它可以列出所写代码的所有语法点并列出微软的说明文档链接\n不过毕竟是网页工具，并不完全支持所有的FCL库，所以可能有些库用不了\n总之这是个非常方便的工具，有什么小测试都可以在上面测试\n第三种方法，ILSpy\n这个工具也挺好用的，有支持Visual Studio的插件\n装上就可以写代码的时候直接反编译\n(@ 23-08-22)\n只是看核心库源代码的话还有一种办法，官方的源代码网站\n具体我没怎么用过，就先补充在这里吧\n我现在在用第二种和第三种方法\n平时做小测试网页工具用用就好了\n但在研究.net的底层api的时候就需要用ILSpy了\n(虽然.net core开源了，但自己找效率确实低，反编译哪里不会点哪里)\n\u0026lt;0x03\u0026gt; C#的编译过程 C#编译到IL分两步\n第一步：编译器优化代码\n第二步：编译成IL\n在第一步中，编译器将对原代码进行彻头彻尾的改造\n比方说代码有一句int a = 5;，在优化过程中会改名为int num = 5;\n还有就是将一些编译期就知道结果的值计算出来，比如\u0026quot;a\u0026quot;+\u0026quot;b\u0026quot;就会直接优化成\u0026quot;ab\u0026quot; 在这个过程中，编译器还会加上非常多的，用于跟CLR通信的指令\n这些指令主要是指导CLR正确执行代码，具体我也不是很熟，以后再说\n第二步编译成IL就看靠编译器了\n这两步具体实现可以看Roslyn的源码，在GitHub上\n(我还没开始研究这玩意)\n另外还有就是C#编译有两种模式\nDebug和Release\n前者编译出的代码并不是最优化的，里面会有很多的nop指令\n(就是执行这条指令啥也不干，方便调试打断点用的)\n后者是用来发布的，编译出来的代码是经过编译器完全优化的\n\u0026lt;0x04\u0026gt; 开始研究IL 首先先是一段简单的代码\nusing System; public class C { public void M() { int a=3; string str=\u0026#34;abc\u0026#34;; Console.WriteLine(a+str); } } 看看它编译后的IL语言(Debug下编译)\n.assembly _ { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) .custom instance void [System.Runtime]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4e 6f 6e 45 78 63 65 70 74 69 6f 6e 54 68 72 6f 77 73 01 ) .custom instance void [System.Runtime]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [System.Runtime]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) .permissionset reqmin = ( 2e 01 80 8a 53 79 73 74 65 6d 2e 53 65 63 75 72 69 74 79 2e 50 65 72 6d 69 73 73 69 6f 6e 73 2e 53 65 63 75 72 69 74 79 50 65 72 6d 69 73 73 69 6f 6e 41 74 74 72 69 62 75 74 65 2c 20 53 79 73 74 65 6d 2e 52 75 6e 74 69 6d 65 2c 20 56 65 72 73 69 6f 6e 3d 37 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 75 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d 62 30 33 66 35 66 37 66 31 31 64 35 30 61 33 61 15 01 54 02 10 53 6b 69 70 56 65 72 69 66 69 63 61 74 69 6f 6e 01 ) .hash algorithm 0x00008004 // SHA1 .ver 0:0:0:0 } .class private auto ansi \u0026#39;\u0026lt;Module\u0026gt;\u0026#39; { } // end of class \u0026lt;Module\u0026gt; .class public auto ansi beforefieldinit C extends [System.Runtime]System.Object { // Methods .method public hidebysig instance void M () cil managed { // Method begins at RVA 0x206c // Code size 29 (0x1d) .maxstack 2 .locals init ( [0] int32 a, [1] string str ) IL_0000: nop IL_0001: ldc.i4.3 IL_0002: stloc.0 IL_0003: ldstr \u0026#34;abc\u0026#34; IL_0008: stloc.1 IL_0009: ldloca.s 0 IL_000b: call instance string [System.Runtime]System.Int32::ToString() IL_0010: ldloc.1 IL_0011: call string [System.Runtime]System.String::Concat(string, string) IL_0016: call void [System.Console]System.Console::WriteLine(string) IL_001b: nop IL_001c: ret } // end of method C::M .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2095 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method C::.ctor } // end of class C .class private auto ansi sealed beforefieldinit Microsoft.CodeAnalysis.EmbeddedAttribute extends [System.Runtime]System.Attribute { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = ( 01 00 00 00 ) // Methods .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x2050 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Attribute::.ctor() IL_0006: nop IL_0007: ret } // end of method EmbeddedAttribute::.ctor } // end of class Microsoft.CodeAnalysis.EmbeddedAttribute .class private auto ansi sealed beforefieldinit System.Runtime.CompilerServices.RefSafetyRulesAttribute extends [System.Runtime]System.Attribute { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void Microsoft.CodeAnalysis.EmbeddedAttribute::.ctor() = ( 01 00 00 00 ) .custom instance void [System.Runtime]System.AttributeUsageAttribute::.ctor(valuetype [System.Runtime]System.AttributeTargets) = ( 01 00 02 00 00 00 02 00 54 02 0d 41 6c 6c 6f 77 4d 75 6c 74 69 70 6c 65 00 54 02 09 49 6e 68 65 72 69 74 65 64 00 ) // Fields .field public initonly int32 Version // Methods .method public hidebysig specialname rtspecialname instance void .ctor ( int32 \u0026#39;\u0026#39; ) cil managed { // Method begins at RVA 0x2059 // Code size 15 (0xf) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Attribute::.ctor() IL_0006: nop IL_0007: ldarg.0 IL_0008: ldarg.1 IL_0009: stfld int32 System.Runtime.CompilerServices.RefSafetyRulesAttribute::Version IL_000e: ret } // end of method RefSafetyRulesAttribute::.ctor } // end of class System.Runtime.CompilerServices.RefSafetyRulesAttribute (注释是Sharpio自动生成的)\n额，看着是有点多，但其实很多只是自动生成的代码\n如果只关注我们自己方法实现的话，实际只要看这么点就好\n.method public hidebysig instance void M () cil managed //方法签名 { .maxstack 2 //定义计算栈大小 .locals init ( [0] int32 a, [1] string str ) //初始化变量链表 IL_0000: nop //啥也不干(Debug下编译) IL_0001: ldc.i4.3 //初始化int32的数值3，并加载到计算栈上 IL_0002: stloc.0 //弹出一个栈顶元素，并存储到变量列表的0号位上(就是变量a) IL_0003: ldstr \u0026#34;abc\u0026#34; //初始化字符串\u0026#34;abc\u0026#34;，并加载到计算栈上 IL_0008: stloc.1 //弹出栈顶元素，存储到变量链表1号上(变量str) IL_0009: ldloca.s 0 //加载变量列表0号位(变量a)到计算栈上 IL_000b: call instance string [System.Runtime]System.Int32::ToString() //调用方法 IL_0010: ldloc.1 //加载变量列表1号位(变量str)到计算栈上 IL_0011: call string [System.Runtime]System.String::Concat(string, string) //调用方法，有几个参就弹出几个栈顶元素，返回值再压入栈 IL_0016: call void [System.Console]System.Console::WriteLine(string) //调用方法 IL_001b: nop //啥也不干 IL_001c: ret //方法结束，返回 } (已经打上了我自己的注释了)\n所以其实也不是挺难的，就是IL的指令差不多都是简写，乍一看确实不明觉厉\n(所以会看IL就可以出去装逼了😅) IL还有很多指令，具体可以看我最上面推荐的两篇博客\n这里的计算栈其实就是个可以放任何类型的栈，计算用的\n具体的CLR执行细节可以看看《CLR via C#》讲的很详细\n","date":"2023-08-03T02:44:36Z","permalink":"/post/articles/2023/%E6%B5%85%E8%B0%88csharp%E8%BD%ACil%E8%AF%AD%E8%A8%80/","title":"浅谈C#转IL语言"},{"content":"\u0026lt;0x00\u0026gt; MVVM模型介绍 MVVM模型并不是一个编程的语法，而是一种设计思路\n整体分三块(M/V/VM)\nM(Model)指数据模型，差不多就是程序的后端部分\nV(View)指界面，就是程序前端\nVM(View Model)，这个不大好翻译，其实就是负责前后端连接\n这样做有什么好处\n简单说就是低耦合高内聚(网上都这么说的)\n换句话说就是修改时更加的灵活，架构更加明白\nMVVM模型是从MVC模型发展过来的，主要解决的就是开发过程中代码结构混乱的问题\n比方说开发一个项目，分前端后端，如果不采用这些设计思路，很可能两人各搞各的\n要连接前后端，既可以是前端控制后端数据，也可以是后端修改前端显示\n这样势必会造成整个项目的混乱，不好管理\n所以MVVM模型引入了VM作为前后端的中间层\n前端的V可以通过VM修改M，后端的M也可以通过VM通知前端的V要界面更新\n这样的话，前后端本身就差不多分离了，前后端开发互不干扰\n反正最后都是靠写一个VM连接在一起的，当然相应的代码量就多了\n总之，MVVM模型不是开发必须的，如果就一个人随便写写，那么肯定是怎么舒服怎么来的\n如果是稍微大一点的项目，不采用合适的开发模式，到时候管理代码有的受的\n\u0026lt;0x01\u0026gt; 如何在WPF中实现MVVM WPF里面不是有些.xaml的文件嘛，这些就是我们的V\n然后在整个工程中，我们还可以新建很多的.cs文件(这个看你用什么.net的语言)\n这些.cs文件就是我们的M和VM\n当然，为了区分方便，通常会在文件后面跟上后缀\n比如*M.cs代表这个文件描述的是一个数据模型\n*VM.cs代表这个文件描述的是一个VM\n反正怎么习惯怎么来就是\n在这篇博客里，我的文件命名没有这么这么规范\n因为就像之前说的，MVVM只是一种设计思路\n\u0026lt;0x02\u0026gt; 正式开始实现 首先先分析我们的需求，就拿我们之前的计算器吧\n前端的实现我们已经有了，就是那个.xaml文件\n后端我们就要先分析下了，怎么搞一个计算器的后端\n首先我们先思考下一个计算器需要维护什么数据\n因为我们打算实现的就是个日常的计算器而不是图形计算器之类的东西\n所以肯定要维护一条当前的结果\n然后还要一个标志符来保存当前选定的运算\n同时还要有个flag来表明正在计算\n最后还要维护一个显示的值\n所以总共是要维护4条数据\n所以对我们的计算器后端文件如下\n//Calculator.cs //常见的引用 namespace WPFTest { public class Calculator { //这里按道理应该用private，通过对象方法调用这些变量，这里偷下懒 internal bool flag = false; //表示正在计算 internal int? calculate = null; //标识当前的运算符 internal double? ans = null; //存储当前的结果 internal string Display = \u0026#34;0\u0026#34;; //当前显示的值 public void Calculate() //计算方法 { switch (calculate) { case 1: ans += Double.Parse(Display); break; case 2: ans -= Double.Parse(Display); break; case 3: ans *= Double.Parse(Display); break; case 4: ans /= Double.Parse(Display); break; } } } } 接下来就是来写我们的VM部分\n我这个写得比较繁琐，但就是那个意思\n//ViewModel.cs //常见的引用 namespace WPFTest { //要实现接口INotifyPropertyChanged public class ViewModel : INotifyPropertyChanged { //实现接口的要求，看不懂没关系，这么写就好 public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if(handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } //声明一个Calculator对象 private readonly Calculator _calculator = new Calculator(); //声明一个Display属性，关键啊 public string Display { get { return _calculator.Display; //定义get，挺正常的写法 } set { _calculator.Display = value; NotifyPropertyChanged(\u0026#34;Display\u0026#34;); //定义set，最后调用的方法通知属性已更改，让前端更新显示 } } public void MinusSign_Click() //减法的实现 { if (_calculator.flag) { if (Display[0] == \u0026#39;-\u0026#39;) { Display = Display.Substring(1, Display.Length - 1); } else { Display = \u0026#34;-\u0026#34; + Display; } } } //后面方法的实现略 public ICommand ButtonMinusSign //将减法方法声明成属性 { get { return new RelayCommand(MinusSign_Click); //RelayCommand后面讲 } } //后面的方法属性声明略 } } Display要声明成属性的样式，因为xaml里面能绑定的量要是属性\n所以后面的按钮方法都要声明成属性，这样才能在xaml里面绑定\n当然这里还出现了两个新东西\n一个是接口INotifyPropertyChanged，还有RelayCommand\nINotifyPropertyChanged字面意思就是通知属性更改\n就是说这个类里面有属性在更改时需要通知前端，让前端显示的东西也跟着改\n这个接口要求实现一个通知方法，反正就按上面的抄就好了\n(大体的原理就是发起一个事件，传回去，让前端知道有东西变了，再回来看)\nRelayCommand是自己实现的类，代码如下\n//RelayCommand.cs //常见的声明 namespace WPFTest { //实现ICommand接口 internal class RelayCommand : ICommand { //一个只读的Action属性，存储指令 public Action ExecuteAction { get; } public event EventHandler CanExecuteChanged; //构造函数 public RelayCommand(Action executeAction) { ExecuteAction = executeAction; } //表示能不能执行(我们这就默认能执行) public bool CanExecute(object parameter) { return true; } //调用方法 public void Execute(object parameter) { ExecuteAction(); } } } 就是这样，本质就是用泛型委托打包了个方法\n这个类也方便我们把方法打包成一个属性，好绑定到xaml中\n最后终于是我们的前端界面了\n其实多的不用改\n\u0026lt;Window x:Class=\u0026#34;WPFTest.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; xmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; xmlns:local=\u0026#34;clr-namespace:WPFTest\u0026#34; d:DataContext=\u0026#34;{d:DesignInstance Type=local:ViewModel}\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; Height=\u0026#34;400\u0026#34; Width=\u0026#34;300\u0026#34; WindowStartupLocation=\u0026#34;CenterScreen\u0026#34; AllowsTransparency=\u0026#34;True\u0026#34; Background=\u0026#34;Transparent\u0026#34; WindowStyle=\u0026#34;None\u0026#34;\u0026gt; \u0026lt;!--这里指定了DataContext，让xaml能找到对应的属性--\u0026gt; \u0026lt;Window.Resources\u0026gt; \u0026lt;Style TargetType=\u0026#34;Button\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;Margin\u0026#34; Value=\u0026#34;6\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;FontSize\u0026#34; Value=\u0026#34;24\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;Foreground\u0026#34; Value=\u0026#34;White\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;Template\u0026#34;\u0026gt; \u0026lt;Setter.Value\u0026gt; \u0026lt;ControlTemplate TargetType=\u0026#34;Button\u0026#34;\u0026gt; \u0026lt;Border Name=\u0026#34;border\u0026#34; Background=\u0026#34;#241238\u0026#34; CornerRadius=\u0026#34;10\u0026#34;\u0026gt; \u0026lt;TextBlock Text=\u0026#34;{TemplateBinding Content}\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34; HorizontalAlignment=\u0026#34;Center\u0026#34;/\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;ControlTemplate.Triggers\u0026gt; \u0026lt;Trigger Property=\u0026#34;IsMouseOver\u0026#34; Value=\u0026#34;True\u0026#34;\u0026gt; \u0026lt;Setter TargetName=\u0026#34;border\u0026#34; Property=\u0026#34;Background\u0026#34; Value=\u0026#34;#190D24\u0026#34;/\u0026gt; \u0026lt;/Trigger\u0026gt; \u0026lt;Trigger Property=\u0026#34;IsPressed\u0026#34; Value=\u0026#34;True\u0026#34;\u0026gt; \u0026lt;Setter TargetName=\u0026#34;border\u0026#34; Property=\u0026#34;Background\u0026#34; Value=\u0026#34;Black\u0026#34;/\u0026gt; \u0026lt;/Trigger\u0026gt; \u0026lt;/ControlTemplate.Triggers\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Setter.Value\u0026gt; \u0026lt;/Setter\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;/Window.Resources\u0026gt; \u0026lt;Border CornerRadius=\u0026#34;10\u0026#34; MouseMove=\u0026#34;Border_MouseMove\u0026#34; Margin=\u0026#34;10\u0026#34;\u0026gt; \u0026lt;Border.Background\u0026gt; \u0026lt;LinearGradientBrush StartPoint=\u0026#34;0,0\u0026#34; EndPoint=\u0026#34;0,1\u0026#34;\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#392669\u0026#34; Offset=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#46204F\u0026#34; Offset=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;/LinearGradientBrush\u0026gt; \u0026lt;/Border.Background\u0026gt; \u0026lt;Border.Effect\u0026gt; \u0026lt;DropShadowEffect Color=\u0026#34;Gray\u0026#34; ShadowDepth=\u0026#34;0\u0026#34; BlurRadius=\u0026#34;10\u0026#34; Opacity=\u0026#34;.5\u0026#34; Direction=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;/Border.Effect\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition Height=\u0026#34;auto\u0026#34;/\u0026gt; \u0026lt;RowDefinition Height=\u0026#34;auto\u0026#34;/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;StackPanel Grid.Row=\u0026#34;0\u0026#34; Orientation=\u0026#34;Horizontal\u0026#34; HorizontalAlignment=\u0026#34;Right\u0026#34;\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Minimize\u0026#34; Click=\u0026#34;Button_Minimize_Click\u0026#34;\u0026gt; \u0026lt;Button.Template\u0026gt; \u0026lt;ControlTemplate\u0026gt; \u0026lt;Ellipse Width=\u0026#34;16\u0026#34; Height=\u0026#34;16\u0026#34; Fill=\u0026#34;#F0DC4E\u0026#34;/\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Button.Template\u0026gt; \u0026lt;/Button\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Close\u0026#34; Click=\u0026#34;Button_Close_Click\u0026#34;\u0026gt; \u0026lt;Button.Template\u0026gt; \u0026lt;ControlTemplate\u0026gt; \u0026lt;Ellipse Width=\u0026#34;16\u0026#34; Height=\u0026#34;16\u0026#34; Fill=\u0026#34;#F0443E\u0026#34;/\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Button.Template\u0026gt; \u0026lt;/Button\u0026gt; \u0026lt;/StackPanel\u0026gt; \u0026lt;TextBlock Grid.Row=\u0026#34;1\u0026#34; Name=\u0026#34;Answer\u0026#34; Text=\u0026#34;{Binding Display}\u0026#34; FontSize=\u0026#34;28\u0026#34; HorizontalAlignment=\u0026#34;Right\u0026#34; Margin=\u0026#34;20\u0026#34; Foreground=\u0026#34;White\u0026#34;/\u0026gt; \u0026lt;!--Text这里绑定的是显示的内容--\u0026gt; \u0026lt;Grid Grid.Row=\u0026#34;2\u0026#34;\u0026gt; \u0026lt;Grid.ColumnDefinitions\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;/Grid.ColumnDefinitions\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;Button Name=\u0026#34;Button_MinusSign\u0026#34; Command=\u0026#34;{Binding ButtonMinusSign}\u0026#34; Content=\u0026#34;±\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;!--Command这里绑定的就是按钮的指令--\u0026gt; \u0026lt;!--后面的Button就不写了--\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/Window\u0026gt; 其实就打注释的那三处要变，别的都不用改\n\u0026lt;0x03\u0026gt; MVVM模型到底有什么用 说实在的，我刚学完这模型我也是说这玩意有啥用\n甚至觉得我是不是少看了些什么\n因为从逻辑上，这么写代码，代码量多了，但功能都没变过\n就感觉这么写很多余\n但实际上，我思考了好一阵(也是这篇博客拖这么久写完的原因)\n这个项目太小了，体现不出来\n想象一个稍微大点的项目，前后端分别开发的\n假设最终就两个主要的文件，一个前端，一个后端\n如果说后端突然抽风要改方法名，那前端也得跟着改(这就是耦合的情况)\n但如果用MVVM模型，在前后端之间再加层\u0026quot;胶水\u0026quot;层\n这样当后端改名的时候，\u0026ldquo;胶水\u0026quot;层的名字没改，前端就不用改，提高了效率\n(这样也叫解耦)\n别的好处我也很难讲了，毕竟目前我的开发经验不多\n正如我最前面写的，这个只是个思想，并不是什么语法之类的\n(总算写完了)\n","date":"2023-07-22T17:39:25Z","permalink":"/post/categories/wpf/wpf%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B003-mvvm%E6%A8%A1%E5%9E%8B/","title":"WPF学习笔记03-MVVM模型"},{"content":"\u0026lt;0x00\u0026gt; WPF的界面美化挺方便的 就研究了一小时不到，成功让计算器的UI上升了一个档次\n先看成果(因为相较于之前的界面确实帅多了)\n现代多了\n(为了显示效果改了下比例)\n咱们一块块慢慢讲\n\u0026lt;0x01\u0026gt; 如何实现自定义窗口控件与圆角窗口 首先看这个界面，比较特殊的就是这个窗口有圆角\n而且右上角的按钮不是Windows的默认样式\n(红色的按钮是关闭，黄色的是最小化窗口)\n要做到这一点，我们要让窗口运行在无边框的模式下\n但运行在无边框模式下后，原先的窗口控件也没有了\n所以要自己写按钮实现窗口关闭和窗口最小化的功能\n(窗口最大化也差不多的逻辑，但这里不需要实现)\n无边框窗口 首先要设置个无边框的窗口\n我们需要在\u0026lt;Window\u0026gt;标签里面加上WindowStyle=\u0026quot;None\u0026quot;就可以实现无边框的窗口了\n(@ 23-07-19)\n其实更正规的做法应该是用WindowChrome，当时不大会，这个只能说是个下策\n这个方法本质上是抛弃了非工作区的控件而在工作区重写这些窗口控件，效率有点低\n\u0026lt;Window x:Class=\u0026#34;WPFTest.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; xmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; xmlns:local=\u0026#34;clr-namespace:WPFTest\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; Height=\u0026#34;400\u0026#34; Width=\u0026#34;300\u0026#34; WindowStartupLocation=\u0026#34;CenterScreen\u0026#34; AllowsTransparency=\u0026#34;True\u0026#34; Background=\u0026#34;Transparent\u0026#34; WindowStyle=\u0026#34;None\u0026#34;\u0026gt; \u0026lt;!--...--\u0026gt; \u0026lt;Window/\u0026gt; (差不多就这样，没讲的属性后面讲)\n这样，我们确实获得了一个无边框的窗口\n但这样有几个问题\n第一，窗口的圆角无法调整\n第二，窗口不能拖动\n解决第一个问题 这意味着我们要自己写一个窗口实现\n(其实不难)\n系统的窗口相当于最下面的图层\n现在这个窗口不能满足我们的需求，所以我们要先隐藏自带的窗口\n在\u0026lt;Window\u0026gt;标签里面加上AllowsTransparency=\u0026quot;True\u0026quot;和Background=\u0026quot;Transparent\u0026quot;\n这两条属性表示允许窗口透明并设置窗口背景为透明\n下面我们要自己写一个背景来代替原来的窗口背景\n我们之前不是拿一个\u0026lt;Grid\u0026gt;包裹所有的控件吗\n既然是背景，那层级肯定是比这个Grid更高的\n所以只需要在\u0026lt;Grid\u0026gt;外面再套个\u0026lt;Border\u0026gt;就好\n(\u0026lt;Border\u0026gt;可以简单理解为绘制一个矩形)\n\u0026lt;!--差不多像这样--\u0026gt; \u0026lt;Window ...\u0026gt; \u0026lt;Border ...\u0026gt; \u0026lt;Grid ...\u0026gt; \u0026lt;!--...--\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/Window\u0026gt; \u0026lt;Border\u0026gt;也可以设置不少的属性\n为了实现窗口圆角，设置下CornerRadius就好\n解决第二个问题 在原生的窗口中，窗口的移动是靠上面的那一条的\n相当于窗口移动事件是绑定在那一条控件上的\n现在我们隐藏了，所以要自己去绑定这个窗口移动事件\n现在刚好写了个\u0026lt;Border\u0026gt;，这个可以绑定窗口移动事件\n在\u0026lt;Border\u0026gt;绑定MouseMove事件(就是添加个属性)\n这个相当于之前给Button绑定Click事件，要在对应.cs里面实现功能\nprivate void Border_MouseMove(object sender, MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { this.DragMove(); } } 加上这段代码就好\n现在这个窗口就能拖动了\n自定义窗口控件按钮 本质上就是自己写个按钮，一个用来关闭程序，一个用来最小化\n(当然为了能放下这俩按钮还需要对页面布局做个小改动，这里就不写了)\n直接贴代码\n//关闭窗口 private void Button_Close_Click(object sender, RoutedEventArgs e) { this.Close(); } //最小化窗口 private void Button_Minimize_Click(object sender, RoutedEventArgs e) { this.WindowState = WindowState.Minimized; } 这两个方法绑定到对应的按钮就好\n\u0026lt;0x02\u0026gt; 如何实现圆角的按钮 要实现圆角的按钮，我们需要重写按钮的绘制方法\n在xaml中，这个重写其实也挺简单的，我们仅需更改按钮的模板\n更改模板的方法有两种，先讲怎么更改单个按钮的模板\n(关闭窗口和最小化窗口的按钮样式就是这么实现的)\n\u0026lt;Button ...\u0026gt; \u0026lt;Button.Template\u0026gt; \u0026lt;ControlTemplate\u0026gt; \u0026lt;!--具体的实现--\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Button.Template\u0026gt; \u0026lt;/Button\u0026gt; 在具体的实现里面写上绘制的样式就行了\n如果有很多的按钮需要更改为一个统一的样式，我们需要在\u0026lt;Style\u0026gt;中更改模板\n\u0026lt;Style TargetType=\u0026#34;Button\u0026#34;\u0026gt; \u0026lt;!--其他Setter--\u0026gt; \u0026lt;Setter Property=\u0026#34;Template\u0026#34;\u0026gt; \u0026lt;Setter.Value\u0026gt; \u0026lt;ControlTemplate TargetType=\u0026#34;Button\u0026#34;\u0026gt; \u0026lt;!--具体的实现--\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Setter.Value\u0026gt; \u0026lt;/Setter\u0026gt; \u0026lt;/Style\u0026gt; 注意，如果模板要绑定属性，TargetType=\u0026quot;Button\u0026quot;不能省\n({TemplateBinding Content}就是个绑定的语法，相当于拷贝Button.Content的内容)\n(具体看原代码吧，绑定确实不大好解释)\n更改过模板后，原来的触发器也给替换掉了，要我们自己再写一套\n(就是比方说鼠标放按钮上，按钮的颜色变深一点之类的)\n这个就要在\u0026lt;ControlTemplate\u0026gt;块中加上\u0026lt;ControlTemplate.Triggers\u0026gt;块\n再在里面写上若干\u0026lt;Trigger\u0026gt;即可\n\u0026lt;Trigger\u0026gt;就用来控制前端的显示，不管后端的\n\u0026lt;Trigger\u0026gt;中有Property(设置触发器类型)和Value(设置触发值)\n常用的Property有IsMouseOn(鼠标在不在上面)和IsPressed(鼠标有没有按下)\n\u0026lt;Trigger\u0026gt;块中通过\u0026lt;Setter\u0026gt;来设置样式\n这里的\u0026lt;Setter\u0026gt;需要加上TargetName(因为可能有好几个需要绘制的东西)\n\u0026lt;0x03\u0026gt; 如何实现背景的色彩渐变 添加色彩渐变很简单\n\u0026lt;Border\u0026gt;块中加上这些代码即可\n\u0026lt;Border.Background\u0026gt; \u0026lt;LinearGradientBrush StartPoint=\u0026#34;0,0\u0026#34; EndPoint=\u0026#34;0,1\u0026#34;\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#392669\u0026#34; Offset=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#46204F\u0026#34; Offset=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;/LinearGradientBrush\u0026gt; \u0026lt;/Border.Background\u0026gt; 其中\u0026lt;LinearGradientBrush\u0026gt;指定了线性渐变\nStartPoint与EndPoint指定了起止点，\u0026quot;0,0\u0026quot;表示左上角，\u0026quot;0,1\u0026quot;表示左下角\n\u0026lt;GradientStop\u0026gt;中可以指定渐变的颜色，Offset相当于颜色在渐变条上的位置\n(如果用过些图像处理工具应该是能理解Offset的功能的)\n\u0026lt;0x04\u0026gt; 绘制窗口阴影 因为我们已经把原生的窗口干掉了嘛，所以窗口阴影也没了\n没窗口阴影的话总是看着有些违和\n添加窗口阴影也很简单\n\u0026lt;Border\u0026gt;块中加上这些代码即可\n\u0026lt;Border.Effect\u0026gt; \u0026lt;DropShadowEffect Color=\u0026#34;Gray\u0026#34; ShadowDepth=\u0026#34;0\u0026#34; BlurRadius=\u0026#34;10\u0026#34; Opacity=\u0026#34;.5\u0026#34; Direction=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;/Border.Effect\u0026gt; 在\u0026lt;Border\u0026gt;中也要调整下Margin属性，不然阴影可能显示不出\n附录 MainWindow.xaml.cs的代码就不贴了，基本没什么改动\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window x:Class=\u0026#34;WPFTest.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; xmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; xmlns:local=\u0026#34;clr-namespace:WPFTest\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; Height=\u0026#34;400\u0026#34; Width=\u0026#34;300\u0026#34; WindowStartupLocation=\u0026#34;CenterScreen\u0026#34; AllowsTransparency=\u0026#34;True\u0026#34; Background=\u0026#34;Transparent\u0026#34; WindowStyle=\u0026#34;None\u0026#34;\u0026gt; \u0026lt;Window.Resources\u0026gt; \u0026lt;Style TargetType=\u0026#34;Button\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;Margin\u0026#34; Value=\u0026#34;6\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;FontSize\u0026#34; Value=\u0026#34;24\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;Foreground\u0026#34; Value=\u0026#34;White\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;Template\u0026#34;\u0026gt; \u0026lt;Setter.Value\u0026gt; \u0026lt;ControlTemplate TargetType=\u0026#34;Button\u0026#34;\u0026gt; \u0026lt;Border Name=\u0026#34;border\u0026#34; Background=\u0026#34;#241238\u0026#34; CornerRadius=\u0026#34;10\u0026#34;\u0026gt; \u0026lt;TextBlock Text=\u0026#34;{TemplateBinding Content}\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34; HorizontalAlignment=\u0026#34;Center\u0026#34;/\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;ControlTemplate.Triggers\u0026gt; \u0026lt;Trigger Property=\u0026#34;IsMouseOver\u0026#34; Value=\u0026#34;True\u0026#34;\u0026gt; \u0026lt;Setter TargetName=\u0026#34;border\u0026#34; Property=\u0026#34;Background\u0026#34; Value=\u0026#34;#190D24\u0026#34;/\u0026gt; \u0026lt;/Trigger\u0026gt; \u0026lt;Trigger Property=\u0026#34;IsPressed\u0026#34; Value=\u0026#34;True\u0026#34;\u0026gt; \u0026lt;Setter TargetName=\u0026#34;border\u0026#34; Property=\u0026#34;Background\u0026#34; Value=\u0026#34;Black\u0026#34;/\u0026gt; \u0026lt;/Trigger\u0026gt; \u0026lt;/ControlTemplate.Triggers\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Setter.Value\u0026gt; \u0026lt;/Setter\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;/Window.Resources\u0026gt; \u0026lt;Border CornerRadius=\u0026#34;10\u0026#34; MouseMove=\u0026#34;Border_MouseMove\u0026#34; Margin=\u0026#34;5\u0026#34;\u0026gt; \u0026lt;Border.Background\u0026gt; \u0026lt;LinearGradientBrush StartPoint=\u0026#34;0,0\u0026#34; EndPoint=\u0026#34;0,1\u0026#34;\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#392669\u0026#34; Offset=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;GradientStop Color=\u0026#34;#46204F\u0026#34; Offset=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;/LinearGradientBrush\u0026gt; \u0026lt;/Border.Background\u0026gt; \u0026lt;Border.Effect\u0026gt; \u0026lt;DropShadowEffect Color=\u0026#34;Gray\u0026#34; ShadowDepth=\u0026#34;0\u0026#34; BlurRadius=\u0026#34;10\u0026#34; Opacity=\u0026#34;.5\u0026#34; Direction=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;/Border.Effect\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition Height=\u0026#34;auto\u0026#34;/\u0026gt; \u0026lt;RowDefinition Height=\u0026#34;auto\u0026#34;/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;StackPanel Grid.Row=\u0026#34;0\u0026#34; Orientation=\u0026#34;Horizontal\u0026#34; HorizontalAlignment=\u0026#34;Right\u0026#34;\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Minimize\u0026#34; Click=\u0026#34;Button_Minimize_Click\u0026#34;\u0026gt; \u0026lt;Button.Template\u0026gt; \u0026lt;ControlTemplate\u0026gt; \u0026lt;Ellipse Width=\u0026#34;16\u0026#34; Height=\u0026#34;16\u0026#34; Fill=\u0026#34;#F0DC4E\u0026#34;/\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Button.Template\u0026gt; \u0026lt;/Button\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Close\u0026#34; Click=\u0026#34;Button_Close_Click\u0026#34;\u0026gt; \u0026lt;Button.Template\u0026gt; \u0026lt;ControlTemplate\u0026gt; \u0026lt;Ellipse Width=\u0026#34;16\u0026#34; Height=\u0026#34;16\u0026#34; Fill=\u0026#34;#F0443E\u0026#34;/\u0026gt; \u0026lt;/ControlTemplate\u0026gt; \u0026lt;/Button.Template\u0026gt; \u0026lt;/Button\u0026gt; \u0026lt;/StackPanel\u0026gt; \u0026lt;TextBlock Grid.Row=\u0026#34;1\u0026#34; Name=\u0026#34;Answer\u0026#34; Text=\u0026#34;0\u0026#34; FontSize=\u0026#34;28\u0026#34; HorizontalAlignment=\u0026#34;Right\u0026#34; Margin=\u0026#34;20\u0026#34; Foreground=\u0026#34;White\u0026#34;/\u0026gt; \u0026lt;Grid Grid.Row=\u0026#34;2\u0026#34;\u0026gt; \u0026lt;Grid.ColumnDefinitions\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;/Grid.ColumnDefinitions\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;Button Name=\u0026#34;Button_MinusSign\u0026#34; Click=\u0026#34;Button_MinusSign_Click\u0026#34; Content=\u0026#34;±\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Clear\u0026#34; Click=\u0026#34;Button_Clear_Click\u0026#34; Content=\u0026#34;CE\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Backspace\u0026#34; Click=\u0026#34;Button_Backspace_Click\u0026#34; Content=\u0026#34;←\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num9\u0026#34; Click=\u0026#34;Button_Num9_Click\u0026#34; Content=\u0026#34;9\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num8\u0026#34; Click=\u0026#34;Button_Num8_Click\u0026#34; Content=\u0026#34;8\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num7\u0026#34; Click=\u0026#34;Button_Num7_Click\u0026#34; Content=\u0026#34;7\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num6\u0026#34; Click=\u0026#34;Button_Num6_Click\u0026#34; Content=\u0026#34;6\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num5\u0026#34; Click=\u0026#34;Button_Num5_Click\u0026#34; Content=\u0026#34;5\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num4\u0026#34; Click=\u0026#34;Button_Num4_Click\u0026#34; Content=\u0026#34;4\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num3\u0026#34; Click=\u0026#34;Button_Num3_Click\u0026#34; Content=\u0026#34;3\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num2\u0026#34; Click=\u0026#34;Button_Num2_Click\u0026#34; Content=\u0026#34;2\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num1\u0026#34; Click=\u0026#34;Button_Num1_Click\u0026#34; Content=\u0026#34;1\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num0\u0026#34; Click=\u0026#34;Button_Num0_Click\u0026#34; Content=\u0026#34;0\u0026#34; Grid.ColumnSpan=\u0026#34;2\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Dot\u0026#34; Click=\u0026#34;Button_Dot_Click\u0026#34; Content=\u0026#34;.\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Div\u0026#34; Click=\u0026#34;Button_Div_Click\u0026#34; Content=\u0026#34;/\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Mul\u0026#34; Click=\u0026#34;Button_Mul_Click\u0026#34; Content=\u0026#34;*\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Sub\u0026#34; Click=\u0026#34;Button_Sub_Click\u0026#34; Content=\u0026#34;-\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Add\u0026#34; Click=\u0026#34;Button_Add_Click\u0026#34; Content=\u0026#34;+\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Equal\u0026#34; Click=\u0026#34;Button_Equal_Click\u0026#34; Content=\u0026#34;=\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Border\u0026gt; \u0026lt;/Window\u0026gt; (经典写博客耗时比写代码多😂)\n","date":"2023-07-17T01:04:58Z","permalink":"/post/categories/wpf/wpf%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B002-%E7%AE%80%E5%8D%95%E7%BE%8E%E5%8C%96%E8%AE%A1%E7%AE%97%E5%99%A8/","title":"WPF学习笔记02-简单美化计算器"},{"content":"\u0026lt;0x00\u0026gt; 我为何选择WPF (@ 23-08-25)\n慢慢学了一个月，总算知道为啥现在用WPF的个人项目不多了\n写简单的UI确实非常方便，但如果要用些现代些的设计就很烦了\n非常容易出现些神秘问题\n这个我也说不明白，大概就是因为之前看到有些项目的窗口就是WPF写的吧\n一定要找个理由就是WPF的支持丰富\n作为一个老UI框架，教程满天飞，更重要的是，用WPF仍可以设计出现代的UI\n(而且相比那些electron框架来说性能更好，只要不搞跨平台)\n目前在Windows下，微软这常见的有四套UI框架\n分别是：WinFrom、WPF、WinUI3、XAUI\nWinFrom实在是老，做一些效果动画会比较费力\nWPF虽然也挺老的，但支持的特性足够做些不错的UI了\nWinUI3新是新，但教程不多，文档支持也比较神秘\nXAUI能跨平台，但微软特色，永不推广，而且也比较新，文档比较神秘\n从WPF开始，微软的UI框架都是要写.xaml的\n所以学会WPF之后，迁移到微软更新的UI框架的学习成本是相对低的\n目前我体验下来WPF算是很好入门的了，只需要会一门.net框架下的语言就可以入门\nxaml语法也不难，可以说看多了也会了\n而且微软的看家本领，UI的可视化开发(Visual Studio)，这对效率提升真的很有帮助\n这篇文章的代码来自我自己随便搞的简易计算器\n(整个程序就花了一个小时写完，足见WPF是多方便)\n\u0026lt;0x01\u0026gt; WPF的典型结构 我这里创建的是基于C#的WPF项目\n创建完我们可以看到如图的项目结构\n差不多就是一个.xaml文件下套.cs文件\n其中在App.xaml文件中，定义了这个应用的基本信息(主要就是启动窗口是哪一个)\nMainWindows.xaml定义了窗口的样式(这个的编辑界面就是一半实时预览窗口，一半是代码编辑器)\n下面的.cs文件可以说描述程序的行为\n简单来说就是.xaml文件管前端(界面)，.cs文件管后端(行为)\n具体的东西后面再讲\n\u0026lt;0x02\u0026gt; XAML的简单语法 xaml这玩意就是参考xml语法的\n如果能看懂xml甚至是html也能看懂xaml\n\u0026lt;!--MainWindow.xaml--\u0026gt; \u0026lt;Window x:Class=\u0026#34;WPFTest.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; xmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; xmlns:local=\u0026#34;clr-namespace:WPFTest\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; Title=\u0026#34;MainWindow\u0026#34; Height=\u0026#34;800\u0026#34; Width=\u0026#34;800\u0026#34;\u0026gt; \u0026lt;Window.Resources\u0026gt; \u0026lt;Style TargetType=\u0026#34;Button\u0026#34;\u0026gt; \u0026lt;Setter Property=\u0026#34;Margin\u0026#34; Value=\u0026#34;5\u0026#34;/\u0026gt; \u0026lt;Setter Property=\u0026#34;FontSize\u0026#34; Value=\u0026#34;30\u0026#34;/\u0026gt; \u0026lt;/Style\u0026gt; \u0026lt;/Window.Resources\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition Height=\u0026#34;auto\u0026#34;/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;Grid Grid.Row=\u0026#34;0\u0026#34;\u0026gt; \u0026lt;TextBlock Name=\u0026#34;Answer\u0026#34; Text=\u0026#34;0\u0026#34; FontSize=\u0026#34;30\u0026#34; HorizontalAlignment=\u0026#34;Right\u0026#34; Margin=\u0026#34;20\u0026#34;/\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;Grid Grid.Row=\u0026#34;1\u0026#34;\u0026gt; \u0026lt;Grid.ColumnDefinitions\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;ColumnDefinition/\u0026gt; \u0026lt;/Grid.ColumnDefinitions\u0026gt; \u0026lt;Grid.RowDefinitions\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;RowDefinition/\u0026gt; \u0026lt;/Grid.RowDefinitions\u0026gt; \u0026lt;Button Name=\u0026#34;Button_MinusSign\u0026#34; Click=\u0026#34;Button_MinusSign_Click\u0026#34; Content=\u0026#34;±\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Clear\u0026#34; Click=\u0026#34;Button_Clear_Click\u0026#34; Content=\u0026#34;CE\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Backspace\u0026#34; Click=\u0026#34;Button_Backspace_Click\u0026#34; Content=\u0026#34;←\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num9\u0026#34; Click=\u0026#34;Button_Num9_Click\u0026#34; Content=\u0026#34;9\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num8\u0026#34; Click=\u0026#34;Button_Num8_Click\u0026#34; Content=\u0026#34;8\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num7\u0026#34; Click=\u0026#34;Button_Num7_Click\u0026#34; Content=\u0026#34;7\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num6\u0026#34; Click=\u0026#34;Button_Num6_Click\u0026#34; Content=\u0026#34;6\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num5\u0026#34; Click=\u0026#34;Button_Num5_Click\u0026#34; Content=\u0026#34;5\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num4\u0026#34; Click=\u0026#34;Button_Num4_Click\u0026#34; Content=\u0026#34;4\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num3\u0026#34; Click=\u0026#34;Button_Num3_Click\u0026#34; Content=\u0026#34;3\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num2\u0026#34; Click=\u0026#34;Button_Num2_Click\u0026#34; Content=\u0026#34;2\u0026#34; Grid.Column=\u0026#34;1\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num1\u0026#34; Click=\u0026#34;Button_Num1_Click\u0026#34; Content=\u0026#34;1\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Num0\u0026#34; Click=\u0026#34;Button_Num0_Click\u0026#34; Content=\u0026#34;0\u0026#34; Grid.ColumnSpan=\u0026#34;2\u0026#34; Grid.Column=\u0026#34;0\u0026#34; Grid.Row=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Dot\u0026#34; Click=\u0026#34;Button_Dot_Click\u0026#34; Content=\u0026#34;.\u0026#34; Grid.Column=\u0026#34;2\u0026#34; Grid.Row=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Div\u0026#34; Click=\u0026#34;Button_Div_Click\u0026#34; Content=\u0026#34;/\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;0\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Mul\u0026#34; Click=\u0026#34;Button_Mul_Click\u0026#34; Content=\u0026#34;*\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Sub\u0026#34; Click=\u0026#34;Button_Sub_Click\u0026#34; Content=\u0026#34;-\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;2\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Add\u0026#34; Click=\u0026#34;Button_Add_Click\u0026#34; Content=\u0026#34;+\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;Button Name=\u0026#34;Button_Equal\u0026#34; Click=\u0026#34;Button_Equal_Click\u0026#34; Content=\u0026#34;=\u0026#34; Grid.Column=\u0026#34;3\u0026#34; Grid.Row=\u0026#34;4\u0026#34;/\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Window\u0026gt; (为了代码高亮正常工作，这里的代码设置为xml的高亮格式)\n一块块掰碎讲\n声明部分 就是最上面一坨不知道什么东西的部分，随便写写的话根本不需要知道这些是啥，别动就好\n(因为我目前也不是很懂这些)\n\u0026lt;Window.Resources\u0026gt; 这里面我就定义了个Button类型的Style\n其实就是规定了Button的一些默认参数\n在下面写Button的时候就不用一个个都写这些东西了\nStyle里面定义了按钮到界面边界的距离(Margin)有5单位\n字体大小(FontSize)为30个单位\n(在WPF里，并不是以像素为单位，而是按DPI来确定实际像素单位大小)\n\u0026lt;Style\u0026gt;里面要指定设置的类型(TargetType)\n在\u0026lt;Style\u0026gt;块中，用\u0026lt;Setter\u0026gt;标签来设置类型下面的属性\n\u0026lt;Setter\u0026gt;标签里面Property选择属性，Value选择值\n\u0026lt;Grid\u0026gt; 在WPF中，有两种常用的界面布局\n分别是：网格布局(\u0026lt;Grid\u0026gt;)和栈布局(\u0026lt;StackPanel\u0026gt;)\n\u0026lt;Grid\u0026gt;可以把一块区域分割成一个网格，网格里面可以塞控件\n\u0026lt;StackPanel\u0026gt;就是个栈，所有控件从上到下(从左到右)排列\n基本上用这两种布局的互相嵌套能解决绝大多数的布局\n我这里的布局选择了\u0026lt;Grid\u0026gt;里面再套一个\u0026lt;Grid\u0026gt;\n具体实现 在\u0026lt;Grid.RowDefinitions\u0026gt;块里面塞两个\u0026lt;RowDefinition/\u0026gt;把Grid分成两行\n(塞几个\u0026lt;RowDefinition/\u0026gt;就是有几行，列同理)\n在一个\u0026lt;RowDefinition/\u0026gt;里加入Height=\u0026quot;auto\u0026quot;表示第一行的高度随内容改变\n(就是跟着第一行的控件高度走)\n然后正式往Grid网格里面塞控件，先第一行\n首先是再塞了个\u0026lt;Grid\u0026gt;，然后在里面塞了个\u0026lt;TextBlock\u0026gt;\n其中，Grid.Row可以指定控件所在的网格行号，列同理\n并定义了一系列的属性参数\n(Name属性就是给.cs调用的对象名，HorizontalAlignment是水平对正方式，设置为右对齐)\n(不是很懂我当时写的逻辑，其实不用多写个\u0026lt;Grid\u0026gt;，直接塞\u0026lt;TextBlock\u0026gt;就好)\n第二行，塞了个\u0026lt;Grid\u0026gt;，并定义出5*4的网格\n下面就是一大堆的按键定义，相对比较无趣\n值得说道的是Click属性，这个会指向对应.cs的一个具体的方法名，实现前后端的融合\n(具体看后端实现就可以了)\n还有按钮0多了个Grid.ColumnSpan=\u0026quot;2\u0026quot;，这个就是让这个按钮可以跨两列显示\n由于有之前的\u0026lt;Style\u0026gt;的定义，所有的按钮都自带字体大小30单位，间距5单位的属性\n这样，我们成功整出了一个简单的计算器界面\n长这样的\n\u0026lt;0x03\u0026gt; 与C#后端的结合 (@ 23-08-14)\n实际上这样并不是搞后端，这些都是再定义前端的行为\n具体搞后端可以看我之后写的讲MVVM设计模型的博客\n但如果项目很小的话这样搞也没问题的\n与C#后端的结合可以说非常简单，就是调用和更改控件对象的成员值就可以\n还记得我们已经在.xaml中为一些控件添加了Name属性吗\n这些Name属性是实现前后端融合的关键\n//等于号按键点击事件的实现逻辑 private void Button_Equal_Click(object sender, RoutedEventArgs e) { if (flag \u0026amp;\u0026amp; ans != null) { Calculate(); flag = false; Answer.Text = ans.ToString(); calculate = null; } } 别的都不重要，还记得我们有一个\u0026lt;TextBlock\u0026gt;的Name属性设置为Answer吗\n通过Answer.Text可以直接访问并修改\u0026lt;TextBlock\u0026gt;的内容，非常方便\n(Answer.Text就是String类型)\n其他的控件也同理\n接下来要处理下按钮的点击事件了\n这个也非常简单，我们之前已经设置了每个按钮的Click属性\n接下来就是在对应的.cs中实现逻辑就好\n//大概的结构 namespace WPFTest { public partial class MainWindow : Window { //... private void Button_MinusSign_Click(object sender, RoutedEventArgs e); //加负号按钮的点击事件 private void Button_Clear_Click(object sender, RoutedEventArgs e); //清屏键的点击事件 private void Button_Backspace_Click(object sender, RoutedEventArgs e); //退格键的点击事件 private void Button_Num9_Click(object sender, RoutedEventArgs e); private void Button_Num8_Click(object sender, RoutedEventArgs e); private void Button_Num7_Click(object sender, RoutedEventArgs e); private void Button_Num6_Click(object sender, RoutedEventArgs e); private void Button_Num5_Click(object sender, RoutedEventArgs e); private void Button_Num4_Click(object sender, RoutedEventArgs e); private void Button_Num3_Click(object sender, RoutedEventArgs e); private void Button_Num2_Click(object sender, RoutedEventArgs e); private void Button_Num1_Click(object sender, RoutedEventArgs e); private void Button_Num0_Click(object sender, RoutedEventArgs e); //各个数字键的点击事件 private void Button_Dot_Click(object sender, RoutedEventArgs e); //小数点按键的点击事件 private void Button_Div_Click(object sender, RoutedEventArgs e); private void Button_Mul_Click(object sender, RoutedEventArgs e); private void Button_Sub_Click(object sender, RoutedEventArgs e); private void Button_Add_Click(object sender, RoutedEventArgs e); //+-*/键的点击事件 private void Button_Equal_Click(object sender, RoutedEventArgs e); //等于号的点击事件 } } 每个按钮的点击事件都相当于在C#中的一个方法\n点击一次就触发一次\n附录 完整的MainWindow.xaml已经贴在上文了，这里就不再放了\n//MainWindow.xaml.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace WPFTest { public partial class MainWindow : Window { bool flag = false; int? calculate = null; double? ans = null; public MainWindow() { InitializeComponent(); } private void Calculate() { switch (calculate) { case 1: ans += Double.Parse(Answer.Text); break; case 2: ans -= Double.Parse(Answer.Text); break; case 3: ans *= Double.Parse(Answer.Text); break; case 4: ans /= Double.Parse(Answer.Text); break; } } private void Button_MinusSign_Click(object sender, RoutedEventArgs e) { if (flag) { if (Answer.Text[0]==\u0026#39;-\u0026#39;) { Answer.Text = Answer.Text.Substring(1, Answer.Text.Length - 1); } else { Answer.Text = \u0026#34;-\u0026#34; + Answer.Text; } } } private void Button_Clear_Click(object sender, RoutedEventArgs e) { flag = false; calculate = null; ans = null; Answer.Text = \u0026#34;0\u0026#34;; } private void Button_Backspace_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text = Answer.Text.Substring(0, Answer.Text.Length - 1); if (Answer.Text.Length == 0) { flag = false; Answer.Text = \u0026#34;0\u0026#34;; } } } private void Button_Num9_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;9\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;9\u0026#34;; } } private void Button_Num8_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;8\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;8\u0026#34;; } } private void Button_Num7_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;7\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;7\u0026#34;; } } private void Button_Num6_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;6\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;6\u0026#34;; } } private void Button_Num5_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;5\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;5\u0026#34;; } } private void Button_Num4_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;4\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;4\u0026#34;; } } private void Button_Num3_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;3\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;3\u0026#34;; } } private void Button_Num2_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;2\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;2\u0026#34;; } } private void Button_Num1_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;1\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;1\u0026#34;; } } private void Button_Num0_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;0\u0026#34;; } else { flag = true; Answer.Text = \u0026#34;0\u0026#34;; } } private void Button_Dot_Click(object sender, RoutedEventArgs e) { if (flag) { Answer.Text += \u0026#34;.\u0026#34;; } } private void Button_Div_Click(object sender, RoutedEventArgs e) { if (flag) { if (ans == null) { ans = Double.Parse(Answer.Text); calculate = 4; flag = false; } else { Calculate(); flag = false; Answer.Text = ans.ToString(); } } else { calculate = 4; } } private void Button_Mul_Click(object sender, RoutedEventArgs e) { if (flag) { if (ans == null) { ans = Double.Parse(Answer.Text); calculate = 3; flag = false; } else { Calculate(); flag = false; Answer.Text = ans.ToString(); } } else { calculate = 3; } } private void Button_Sub_Click(object sender, RoutedEventArgs e) { if (flag) { if (ans == null) { ans = Double.Parse(Answer.Text); calculate = 2; flag = false; } else { Calculate(); flag = false; Answer.Text = ans.ToString(); } } else { calculate = 2; } } private void Button_Add_Click(object sender, RoutedEventArgs e) { if (flag) { if (ans == null) { ans = Double.Parse(Answer.Text); calculate = 1; flag = false; } else { Calculate(); flag = false; Answer.Text = ans.ToString(); } } else { calculate = 1; } } private void Button_Equal_Click(object sender, RoutedEventArgs e) { if (flag \u0026amp;\u0026amp; ans != null) { Calculate(); flag = false; Answer.Text = ans.ToString(); calculate = null; } } } } 逻辑就是随性写的，不保证完全没Bug，应该是没严重Bug的\n(所以也没写高精度的运算处理)\n(写这篇博客比我写代码的时间都长😂)\n","date":"2023-07-15T20:51:01Z","permalink":"/post/categories/wpf/wpf%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B001-%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%E8%AE%A1%E7%AE%97%E5%99%A8/","title":"WPF学习笔记01-一个简单的计算器"},{"content":"前言 一般来说离散的课设都是给一大堆题目然后自己选的\n我选的是合式公式的真值表和成真成假赋值\n算是里面相对难的\n(别的是真挺简单的)\n课设我是拿C#写的(人生苦短，还是用好用的语言吧)\n应该常用的.net版本都能跑起来吧，代码里没怎么用新特性\n(我用的是.net 6.0)\nC#的语法还是挺好懂得，用过C系语言看懂应该问题不大\n同样，可以在本文最后复制所有的原码\n(就一个文件，就不发本站的gitea了)\n(报告自己写写挺快的)\n\u0026lt;0x00\u0026gt; 如何解析合式公式 在尝试解析合式公式前，我们可以先想想我们该如何解析一般加减乘除的\n(所谓解析就是判断计算的优先级)\n比方说我们有公式1+2*3/(4-5)\n按运算优先级加上括号就是(1+((2*3)/(4-5)))\n所以说我们该如何让计算机按这样的优先级计算呢\n双栈法\n准备两个栈，一个是运算数栈，一个是运算符栈\n运算式在运算前先在首位各加括号(确保最后会栈空)\n然后从左向右遍历运算式\n遇到(时直接压入运算符栈，遇到数字压入运算数栈\n当遇到运算符时，先检查当前运算符优先级是否大于栈顶运算符，如是，直接压入\n如果不是，那么就先进行弹出操作\n每次弹出都是弹出一个运算符与两个运算数，并将运算结果再压入运算符栈中\n重复弹出，直到当前运算符优先级大于运算符栈顶的运算符优先级为止\n如果遍历过程中遇到)，则反复进行弹出操作，直到弹出一个(为止\n同样的思路也可以运用在合式公式上\n具体流程如下：\n1 给公式的左右套上括号 2 从左向右读取 3 如果遇到(，将(压入运算符栈后继续 4 如果遇到运算数，压入操作数栈后继续 5 如果遇到运算符，按照以下规则： a 如果运算符栈为空或栈顶元素优先级小于当前运算符，直接压入运算符栈 b 若遇到栈顶元素为┐(非)，需要弹出所有的连续┐(非)，弹出此运算符再弹出一个运算数，并将运算结果压回操作数栈 c 其余情况按照弹出一个运算符并弹出两个操作数的规则，反复弹出，并将结合结果压回操作数栈，直到能按照5.a压入运算符栈 6 遇到)时，反复按照规则弹出，直到运算符栈顶为(，最后移除栈顶的( 7 读取完毕后，操作数栈留下的东西就是公式答案 这个就不再用动画演示了，本质上就是一般+-*/运算双栈法的变种\n唯一不一样的就是对于┐(非)要注意连续弹出问题\n因为┐(非)是一个右结合单目运算符，而且优先级最高\n如果不先处理调连续的┐(非)容易解析出问题\n所以要把连续的┐(非)全弹出，当作一个整体，这样才能出正确答案\n\u0026lt;0x01\u0026gt; 由双栈法构建运算树 目前我们已经有方法解析合式公式了，但有一个问题\n我们的题目要求给出一个真值表\n如果每次计算我们都要先解析一遍合式公式的话，效率不会很高\n所以我们要用一种数据结构来保存计算的结构\n每个单一操作数数可以改变对应的真假指派(就是T/F)\n对于一个计算的过程，我们可以抽象成一个树\n所有的操作数都是树的叶子(最末端的节点)\n操作数由运算符连接，合成一个新节点\n如此嵌套，最后连接在一个根节点上\n每次计算时，先给操作数真假指派，然后走遍计算树，根节点的值就是公式的计算结果\n就是这么个结构\n怎么构建这棵树呢，其实之前的双栈法里就有这个答案\n我们仅需略微更改下代码，操作数栈不存储运算结果而是存储运算节点\n这样走一遍公式就相当于构建了一颗计算树\n最后只要存储根节点就好了\n\u0026lt;0x02\u0026gt; 逻辑实现 按从低到高的优先级列出运算符的枚举类型 public enum Operator//枚举运算符 { LeftBracket,//左括号 XNOR,//双条件运算符 Odds,//条件运算符 Or,//析取运算符 And,//合取运算符 Not,//取反运算符 } 定义计算树的计算节点类(仅列出定义) internal class CalculateNode//定义计算节点 { CalculateNode? left;//运算符左边 CalculateNode? right;//运算符右边 Operator oper;//节点的运算符 bool ans;//该节点答案 internal CalculateNode(bool ans);//末节点构造方法 internal CalculateNode(CalculateNode? left, CalculateNode right, Operatoroper);//计算节点构造方法 static bool Odds(bool a, bool b);//单独定义条件运算符 bool Calculate(CalculateNode node);//从某节点递归计算 internal bool Calculate();//提供一个入口 internal void ChangeAns(bool ans);//改变末节点的值 } 可以看到，每个计算节点包含运算符，左边节点，右边节点，节点答案\n操作数节点仅有节点答案(所以左右节点的类型是可空类型)\n因为条件运算符没有现成的运算符，所以自己写一个\nCalculate(CalculateNode node)通过递归来遍历每个节点并给出每个节点的答案\nCalculate()专门给外部调用的，里面就一句return Calculate(this);\nChangeAns(bool ans)用来改变操作数的真假指派\n定义WellFormedFormula类(仅列出定义) public class WellFormedFormula { string formula;//原公式 Dictionary\u0026lt;string, CalculateNode\u0026gt; map;//以string方式索引所有节点 Dictionary\u0026lt;int, string\u0026gt; dic;//以int方式索引所有根节点的名字 CalculateNode root;//最终答案节点 bool[] truthTable;//真值表 private static bool IsOperator(char c);//判断是否为二元运算符 private static char OperatorToChar(Operator op);//运算符换成char private static Operator CharToOperator(char c);//char换成运算符 private bool Verify();//验证公式是否有效 private void Init();//初始化计算树 public WellFormedFormula(string formula);//构造方法 public bool Calculate(int n); public bool Calculate(Dictionary\u0026lt;string, bool\u0026gt; keyValuePairs);//单独计算用 private void CalculateAll();//全计算 public void ChangeFormula(string formula);//更改公式 public void ShowrTruthTable();//展示真值表 public void ShowFormula();//展示公式 public void ShowAllNode();//展示所有节点信息，用于调试 public void ShowTrueAssignment();//展示所有成真赋值 public void ShowFalseAssignment();//展示所有成假赋值 public void ShowDNF();//主析取范式 public void ShowCNF();//主合取范式 } 最关键的是Init()方法，别的实现不难\nprivate void Init()//初始化计算树(略去实现细节) { Verify();//验证 //获取所有的变量 //... //构建计算树 //... CalculateAll();//顺便把真值表给算了 } 在Init()中，先验证公式合法性(防输入错误)\n然后会先遍历遍公式，获取所有的变量名并创建操作数节点(末节点)\n然后再遍历一遍，构建计算树\n构建完成后，root指向根节点，map可以按节点名索引到对应的操作数节点\ndic可以按index索引到节点名字(方便遍历)\n构建完再计算所有的赋值情况，直接得出真值表\n其他的方法都是调用真值表答案，不会再计算公式\n构建过程就是双栈法构建\n附录，所有的代码 //核心逻辑实现 namespace CStest { public enum Operator//枚举运算符 { LeftBracket,//左括号 XNOR,//双条件运算符 Odds,//条件运算符 Or,//析取运算符 And,//合取运算符 Not,//取反运算符 } internal class CalculateNode//定义计算节点 { CalculateNode? left;//运算符左边 CalculateNode? right;//运算符右边 Operator oper;//节点的运算符 bool ans;//该节点答案 internal CalculateNode(bool ans)//末节点构造方法 { left = null; right = null; this.ans = ans; } internal CalculateNode(CalculateNode? left, CalculateNode right, Operator oper)//计算节点构造方法 { this.left = left; this.right = right; this.oper = oper; } static bool Odds(bool a, bool b)//单独定义条件运算符 { if (!a) { return true; } else { if (b) { return true; } else { return false; } } } bool Calculate(CalculateNode node)//从某节点递归计算 { if (node.right == null)//递归边界条件 { return node.ans; } switch (node.oper) { case Operator.Not: node.ans = !Calculate(node.right); break; case Operator.And: node.ans = Calculate(node.left) \u0026amp;\u0026amp; Calculate(node.right); break; case Operator.Or: node.ans = Calculate(node.left) || Calculate(node.right); break; case Operator.Odds: node.ans = Odds(Calculate(node.left), Calculate(node.right)); break; case Operator.XNOR: node.ans = !(Calculate(node.left) ^ Calculate(node.right)); break; } return node.ans; } internal bool Calculate()//提供一个入口 { return Calculate(this); } internal void ChangeAns(bool ans)//改变末节点的值 { this.ans = ans; } } public class WellFormedFormula { string formula;//原公式 Dictionary\u0026lt;string, CalculateNode\u0026gt; map;//以string方式索引所有节点 Dictionary\u0026lt;int, string\u0026gt; dic;//以int方式索引所有根节点的名字 CalculateNode root;//最终答案节点 bool[] truthTable;//真值表 private static bool IsOperator(char c)//判断是否为二元运算符 { return c == \u0026#39;∧\u0026#39; || c == \u0026#39;∨\u0026#39; || c == \u0026#39;→\u0026#39; || c == \u0026#39;⊙\u0026#39;; } private static char OperatorToChar(Operator op)//运算符换成char { switch (op) { case Operator.And: return \u0026#39;∧\u0026#39;; case Operator.Or: return \u0026#39;∨\u0026#39;; case Operator.Odds: return \u0026#39;→\u0026#39;; case Operator.XNOR: return \u0026#39;⊙\u0026#39;; } return \u0026#39; \u0026#39;; } private static Operator CharToOperator(char c)//char换成运算符 { switch (c) { case \u0026#39;∧\u0026#39;: return Operator.And; case \u0026#39;∨\u0026#39;: return Operator.Or; case \u0026#39;→\u0026#39;: return Operator.Odds; case \u0026#39;⊙\u0026#39;: return Operator.XNOR; } return Operator.LeftBracket; } private bool Verify()//验证公式是否有效 { if (IsOperator(formula[0]) || formula[^1] == \u0026#39;┐\u0026#39; || IsOperator(formula[^1])) { throw new Exception(\u0026#34;Error in formula\u0026#34;);//在首尾出现了不该出现的运算符 } int l = 0;//统计左括号 int r = 0;//统计右括号 for (int i = 0; i \u0026lt; formula.Length; i++) { if (formula[i] == \u0026#39;(\u0026#39;) { l++; } else if (formula[i] == \u0026#39;)\u0026#39;) { r++; } if (formula[i] == \u0026#39;┐\u0026#39;) { if (IsOperator(formula[i + 1])) { throw new Exception(\u0026#34;Error in formula\u0026#34;);//取反运算符后接别的运算符 } } if (IsOperator(formula[i])) { if (IsOperator(formula[i - 1])) { throw new Exception(\u0026#34;Error in formula\u0026#34;); } if (IsOperator(formula[i + 1]) || formula[i + 1] == \u0026#39;┐\u0026#39;) { throw new Exception(\u0026#34;Error in formula\u0026#34;); } }//都是判断多个运算符不应该相连的情况 } if (l != r) { throw new Exception(\u0026#34;Error in formula\u0026#34;);//左右括号数不匹配 } return true; } private void Init()//初始化计算树 { Verify();//验证 string formula = \u0026#34;(\u0026#34; + this.formula + \u0026#34;)\u0026#34;; //获取所有的变量 int index = 0; bool flag = true; for (int i = 0; i \u0026lt; formula.Length; i++) { if (flag \u0026amp;\u0026amp; formula[i] != \u0026#39;(\u0026#39; \u0026amp;\u0026amp; formula[i] != \u0026#39;┐\u0026#39; \u0026amp;\u0026amp; !IsOperator(formula[i])) { index = i; flag = false; } else if (formula[i] == \u0026#39;)\u0026#39; || formula[i] == \u0026#39;┐\u0026#39; || IsOperator(formula[i])) { if (index != i \u0026amp;\u0026amp; !flag \u0026amp;\u0026amp; !map.ContainsKey(formula[index..i])) { map.Add(formula[index..i], new CalculateNode(false)); dic.Add(dic.Count, formula[index..i]); } flag = true; } } flag = true; //构建计算树 Stack\u0026lt;string\u0026gt; v = new Stack\u0026lt;string\u0026gt;();//操作数栈 Stack\u0026lt;Operator\u0026gt; op = new Stack\u0026lt;Operator\u0026gt;();//运算符栈 string sa; string sb;//两个临时string变量 for (int i = 0; i \u0026lt; formula.Length; i++) { if (formula[i] == \u0026#39;(\u0026#39;)//左括号直接压入栈 { op.Push(Operator.LeftBracket); continue; } if (flag \u0026amp;\u0026amp; formula[i] != \u0026#39;)\u0026#39; \u0026amp;\u0026amp; formula[i] != \u0026#39;┐\u0026#39; \u0026amp;\u0026amp; !IsOperator(formula[i])) { index = i; flag = false; } else if (formula[i] == \u0026#39;)\u0026#39; || formula[i] == \u0026#39;┐\u0026#39; || IsOperator(formula[i])) { if (index != i \u0026amp;\u0026amp; !flag)//压入操作数 { v.Push(formula[index..i]); flag = true; } if (formula[i] == \u0026#39;┐\u0026#39;) { op.Push(Operator.Not);//取反直接压入 } else if (formula[i] == \u0026#39;)\u0026#39;)//遇到右括号不断弹出，直到遇到左括号 { while (op.Peek() != Operator.LeftBracket) { if (op.Peek() == Operator.Not) { sa = v.Pop(); map.Add(\u0026#34;(┐\u0026#34; + sa + \u0026#34;)\u0026#34;, new CalculateNode(null, map[sa], Operator.Not)); v.Push(\u0026#34;(┐\u0026#34; + sa + \u0026#34;)\u0026#34;); break; } else { sa = v.Pop(); sb = v.Pop(); map.Add(\u0026#34;(\u0026#34; + sb + OperatorToChar(op.Peek()) + sa + \u0026#34;)\u0026#34;, new CalculateNode(map[sb], map[sa], op.Peek())); v.Push(\u0026#34;(\u0026#34; + sb + OperatorToChar(op.Peek()) + sa + \u0026#34;)\u0026#34;); } op.Pop(); } op.Pop();//弹出左括号 } else { if (op.Count != 0 \u0026amp;\u0026amp; CharToOperator(formula[i]) \u0026lt; op.Peek()) { if (op.Peek() == Operator.Not) { do//由于取反运算可多个连接，所以要一次性弹出所有连续的取反运算 { op.Pop(); sa = v.Pop(); map.Add(\u0026#34;(┐\u0026#34; + sa + \u0026#34;)\u0026#34;, new CalculateNode(null, map[sa], Operator.Not)); v.Push(\u0026#34;(┐\u0026#34; + sa + \u0026#34;)\u0026#34;); } while (op.Peek() == Operator.Not); } else { sa = v.Pop(); sb = v.Pop(); map.Add(\u0026#34;(\u0026#34; + sb + OperatorToChar(op.Peek()) + sa + \u0026#34;)\u0026#34;, new CalculateNode(map[sb], map[sa], op.Peek())); v.Push(\u0026#34;(\u0026#34; + sb + OperatorToChar(op.Peek()) + sa + \u0026#34;)\u0026#34;); op.Pop(); } } op.Push(CharToOperator(formula[i]));//最后把当前运算符压入 } } } root = map[v.Pop()];//最后的操作数就是root节点 truthTable = new bool[(int)Math.Pow(2, dic.Count)]; CalculateAll();//顺便把真值表给算了 } public WellFormedFormula(string formula)//构造方法 { map = new Dictionary\u0026lt;string, CalculateNode\u0026gt;(); dic = new Dictionary\u0026lt;int, string\u0026gt;(); this.formula = formula; Init(); } public bool Calculate(int n) { return truthTable[n]; } public bool Calculate(Dictionary\u0026lt;string, bool\u0026gt; keyValuePairs)//单独计算用 { if (keyValuePairs.Count != dic.Count) { throw new Exception(); } foreach (var kv in keyValuePairs) { map[kv.Key].ChangeAns(kv.Value); } return root.Calculate(); } private void CalculateAll()//全计算 { Stack\u0026lt;bool\u0026gt; b = new Stack\u0026lt;bool\u0026gt;(); int index; for (int i = 0; i \u0026lt; (int)Math.Pow(2, dic.Count); i++) { b.Clear(); index = i; while (index \u0026gt; 0) { if (index % 2 == 1) { b.Push(true); } else { b.Push(false); } index /= 2; } while (b.Count \u0026lt; dic.Count) { b.Push(false); } for (int j = 0; j \u0026lt; dic.Count; j++) { map[dic[j]].ChangeAns(b.Pop()); } truthTable[i] = root.Calculate(); } } public void ChangeFormula(string formula)//更改公式 { this.formula = formula; map.Clear(); dic.Clear(); Init(); } public void ShowrTruthTable()//展示真值表 { for (int i = 0; i \u0026lt; dic.Count; i++) { Console.Write(dic[i] + \u0026#39;\\t\u0026#39;); } Console.WriteLine(formula); Stack\u0026lt;bool\u0026gt; b = new Stack\u0026lt;bool\u0026gt;(); int index; for (int i = 0; i \u0026lt; (int)Math.Pow(2, dic.Count); i++) { index = i; while (index \u0026gt; 0) { if (index % 2 == 1) { b.Push(true); } else { b.Push(false); } index /= 2; } while (b.Count \u0026lt; dic.Count) { b.Push(false); } while (b.Count \u0026gt; 0) { if (b.Pop()) { Console.Write(\u0026#34;T\\t\u0026#34;); } else { Console.Write(\u0026#34;F\\t\u0026#34;); } } if (truthTable[i]) { Console.WriteLine(\u0026#34;T\u0026#34;); } else { Console.WriteLine(\u0026#34;F\u0026#34;); } } } public void ShowFormula()//展示公式 { Console.WriteLine(formula); } public void ShowAllNode()//展示所有节点信息，用于调试 { Console.Write(\u0026#34;| \u0026#34;); foreach (var m in map) { Console.Write(m.Key + \u0026#34; | \u0026#34;); } Console.WriteLine(); } public void ShowTrueAssignment()//展示所有成真赋值 { Console.Write(\u0026#34;All true assignment: \u0026#34;); for (int i = 0; i \u0026lt; dic.Count; i++) { if (i == 0) { Console.Write(dic[i]); } else { Console.Write(\u0026#34; | \u0026#34; + dic[i]); } } Console.WriteLine(); int index; Stack\u0026lt;byte\u0026gt; temp = new Stack\u0026lt;byte\u0026gt;(); for (int i = 0; i \u0026lt; (int)Math.Pow(2, dic.Count); i++) { temp.Clear(); if (truthTable[i]) { index = i; while (index \u0026gt; 0) { temp.Push((byte)(index % 2)); index /= 2; } while (temp.Count \u0026lt; dic.Count) { temp.Push(0); } while (temp.Count \u0026gt; 0) { Console.Write(temp.Pop()); } Console.WriteLine(\u0026#34;(\u0026#34; + i + \u0026#34;)\u0026#34;); } } } public void ShowFalseAssignment()//展示所有成假赋值 { Console.Write(\u0026#34;All false assignment: \u0026#34;); for (int i = 0; i \u0026lt; dic.Count; i++) { if (i == 0) { Console.Write(dic[i]); } else { Console.Write(\u0026#34; | \u0026#34; + dic[i]); } } Console.WriteLine(); int index; Stack\u0026lt;byte\u0026gt; temp = new Stack\u0026lt;byte\u0026gt;(); for (int i = 0; i \u0026lt; (int)Math.Pow(2, dic.Count); i++) { temp.Clear(); if (!truthTable[i]) { index = i; while (index \u0026gt; 0) { temp.Push((byte)(index % 2)); index /= 2; } while (temp.Count \u0026lt; dic.Count) { temp.Push(0); } while (temp.Count \u0026gt; 0) { Console.Write(temp.Pop()); } Console.WriteLine(\u0026#34;(\u0026#34; + i + \u0026#34;)\u0026#34;); } } } public void ShowDNF()//主析取范式 { int count = 0; for (int i = 0; i \u0026lt; (int)Math.Pow(2, dic.Count); i++) { if (truthTable[i]) { if (count == 0) { Console.Write(\u0026#34;m(\u0026#34; + i + \u0026#34;)\u0026#34;); } else { Console.Write(\u0026#34;∨m(\u0026#34; + i + \u0026#34;)\u0026#34;); } count++; } } Console.WriteLine(); } public void ShowCNF()//主合取范式 { int count = 0; for (int i = 0; i \u0026lt; (int)Math.Pow(2, dic.Count); i++) { if (!truthTable[i]) { if (count == 0) { Console.Write(\u0026#34;M(\u0026#34; + i + \u0026#34;)\u0026#34;); } else { Console.Write(\u0026#34;∧M(\u0026#34; + i + \u0026#34;)\u0026#34;); } count++; } } Console.WriteLine(); } } } //main.cs namespace CTest{ public class test { public static void Main(String[] args) { WellFormedFormula w = new WellFormedFormula(\u0026#34;(P∨Q)∧(P→R)\u0026#34;); w.ShowAllNode(); w.ShowrTruthTable(); w.ShowTrueAssignment(); w.ShowFalseAssignment(); w.ShowCNF(); w.ShowDNF(); } } } ","date":"2023-07-04T13:14:00Z","permalink":"/post/articles/2023/%E6%B5%99%E5%B7%A5%E5%A4%A7%E7%A6%BB%E6%95%A3%E8%AF%BE%E8%AE%BE%E8%AE%B0%E5%BD%95/","title":"浙工大离散课设讲解"},{"content":"\u0026lt;0x00\u0026gt; 前言 我的题目是幼儿园的信息管理系统(第29号)\n运行在Cmake 3.25.3+mingw 12.2.0上\n基本要求： 能从文件读出一个班级小朋友的信息，并能将管理信息保存到文件 小朋友的信息包括：姓名、出生年月（年龄）、性别、住址、身高、体重、父母联系人姓名、联系电话，备注信息等 功能要求： 1、系统以菜单方式工作； 2、增加修改和删除小朋友信息； 3、查询某个小朋友的父母联系人姓名及联系电话；查询小朋友住址； 4、显示所有小朋友信息：能根据小朋友的身高、体重分别排序，或者按身高体重比排序； 5、能按性别统计班级男女生人数比例； 6、可以进行小朋友信息的文件读写。 实现上面功能是挺简单，所以我加入了一些新功能\n于是就实现了些杂七杂八的东西(之后也会重点介绍的):\n双向模板链表 方向键界面控制 csv的文件读取和导出 多文件管理 从临时文件恢复 本来想实现更多的，实际上摸了就没写😋\n(主要是真没啥好写的)\n你可以在本站gitea下载源代码\n(课设报告就不发了，自己写写挺快的)\n\u0026lt;0x01\u0026gt; 双向链表实现 //Mylist.cpp 仅列出声明，完整代码见源文件 #pragma once #include \u0026lt;cstddef\u0026gt; //NULL的定义在这个头文件里(我用的是mingw) using namespace std; template \u0026lt;class T\u0026gt; class MyList; //提前声明类型 template \u0026lt;class T\u0026gt; class MyNode { T content; // 存放内容 MyNode\u0026lt;T\u0026gt; *next; // 指向下一个Node MyNode\u0026lt;T\u0026gt; *back; // 指向前一个Node friend class MyList\u0026lt;T\u0026gt;; // 为List类提供访问权限(方便) friend class Tool; // 为Tool类提供访问权限(方便) MyNode(); public: MyNode(const T \u0026amp;content, MyNode *next = NULL, MyNode *back = NULL); MyNode(const MyNode \u0026amp;n); //一些节点的构造函数 MyNode\u0026lt;T\u0026gt; \u0026amp;operator=(const MyNode \u0026amp;n); //保险起见，重载下赋值运算 }; template \u0026lt;class T\u0026gt; class MyList { int length; // List的长度 MyNode\u0026lt;T\u0026gt; *head; // 指向头节点 MyNode\u0026lt;T\u0026gt; *end; // 指向尾节点 MyNode\u0026lt;T\u0026gt; *Find(int index) const; // 内部按index搜索用 friend class Tool; // 为Tool类提供访问权限(懒得写访问的函数了) public: MyList(); bool Insert(const T \u0026amp;c, int n = 0); // 插入List，可指定插入位置 bool Delete(int index); // 按index删除List元素 void Clear(); // 清空List内容 int GetLength() const; // 返回List的长度 MyList\u0026lt;T\u0026gt; \u0026amp;operator=(const MyList\u0026lt;T\u0026gt; \u0026amp;n); //保险起见，重载下赋值运算 T *operator[](int index) const; // 外部按index搜索用 ~MyList(); // 析构是门学问 }; 泛型类 MyNode\u0026lt;T\u0026gt; 主要就是存放内容物的节点，为了数据结构的可拓展性，就写了个泛型\n基本就没啥好说的，关键就是用指针关系吧各个节点连接起来\n我这里采用的是双向链表的数据结构，即每个节点可以找到它的前一个后一个\n所以我这里的MyNode类需要两个指针，一个指前一个节点，一个指后一个节点\n泛型类 MyList\u0026lt;T\u0026gt; 说白了，这部分的内容就是把各个节点拼起来\n为了首尾插入效率与保险起见，这个双向列表带有空的首尾节点\n(但最终代码没有完成对末尾插入的优化，忘了\n总之，数据结构图如下\n每个节点都可以找到它的前后节点\n虽然对插入操作没什么优势，但可以加速删除操作\n对于删除操作，仅需传入节点位置即可删除\n相当于把要删除的东西抽出来，再两边节点建立连接\n(单向链表的删除比较抽象)\n对于Clear()操作，相当于走一遍链表，边走边删对象\n走完了再回归到初始状态\n析构就是Clear()后再把首尾节点也删了\n剩下的都挺直观的，也不说了\n\u0026lt;0x02\u0026gt; 方向键控制 方向键控制的代码写在UI.hpp里\nUI.hpp里面都是一堆静态函数，提供一些标准化输出实现\n(压缩代码量)\n//能实现方向控制的示例代码 #include \u0026lt;conio.h\u0026gt; //必要的头文件 using namespace std; int main() { char c; c = _getch(); swich(c) { case 72: //上方向键的代码 case 80: //下方向键的代码 case 75: //左方向键的代码 case 77: //右方向建的代码 case 13: //回车键的代码 } return 0; } 关键就是_getch()这个函数，他会捕获命令行界面的按键，然后返回对应按键的编码\n其他的按键编码网上应该都能找到的\n顺带讲下UI.hpp里的东西\n结构体 UIInfo 就是用来传递菜单信息的结构体\n好让主函数知道进行了什么操作，选了哪一个\nUI类 里面都是静态函数\nConfirm(...)函数用来显示确认菜单\nyesText写确认的文字\nnoText写取消的文字\ncontent写显示的内容\nSelect(...)函数显示选择菜单\n有三个重载\n总之可以显示标题，功能选择项，一般选择项，描述词，默认指针位置\ntitle标题\nfunction功能选择项\ncontent一般选择项\nhead描述词\nindex默认指针位置\n一般选择项十个每页，功能选择项始终显示\nShow(...)函数就是个简单的输出函数\n没啥好说的，让输出好看点而已\n\u0026lt;0x03\u0026gt; CSV文件的读写 CSV文件的读写代码写在ManageTool.hpp里\n//导出为CSV文件 //原代码的逻辑已经嵌在相关函数里了，这里单独拿出来 #include \u0026lt;fstream\u0026gt; #include \u0026lt;cstddef\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026#34;MyClass.hpp\u0026#34; #include \u0026#34;MyDate.hpp\u0026#34; #include \u0026#34;UI.hpp\u0026#34; using namespace std; string temp; //当前文件路径 MyClass *selectClass; //MyClass是我写的班级类 ofstream writeFile; //写入文件流 void ExportCSVFile() { writeFile.open(temp.substr(0, temp.length() - 4) + \u0026#34;-\u0026#34; + selectClass-\u0026gt;className + \u0026#34;.csv\u0026#34;, ios::out); writeFile \u0026lt;\u0026lt; \u0026#34;Class name: \u0026#34; \u0026lt;\u0026lt; selectClass-\u0026gt;className \u0026lt;\u0026lt; \u0026#34;,\u0026#34; \u0026lt;\u0026lt; selectClass-\u0026gt;children.GetLength() \u0026lt;\u0026lt; \u0026#34;,\\n\u0026#34;; writeFile \u0026lt;\u0026lt; \u0026#34;Name,Birthday,Age,Gender,Address,Height,Weight,Parent\u0026#39;s name,Parent\u0026#39;s phone,note,\\n\u0026#34;; for (int i = 0; i \u0026lt; selectClass-\u0026gt;children.GetLength(); i++) { writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;name \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;birthday.GetDate() \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;age \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; if (selectClass-\u0026gt;children[i]-\u0026gt;isBoy) { writeFile \u0026lt;\u0026lt; \u0026#34;Boy,\u0026#34;; } else { writeFile \u0026lt;\u0026lt; \u0026#34;Girl,\u0026#34;; } writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;address \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;height \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;weight \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;parentName \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;parentPhone \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; selectClass-\u0026gt;children[i]-\u0026gt;note \u0026lt;\u0026lt; \u0026#34;,\u0026#34;; writeFile \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } writeFile.close(); UI::Show(\u0026#34;CSV file has been generate\u0026#34;); break; } //由CSV文件导入 //原代码的逻辑已经嵌在相关函数里了，这里单独拿出来 #include \u0026lt;fstream\u0026gt; #include \u0026lt;cstddef\u0026gt; #include \u0026lt;string\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026#34;MyClass.hpp\u0026#34; #include \u0026#34;MyDate.hpp\u0026#34; #include \u0026#34;UI.hpp\u0026#34; using namespace std; string path; //路径 MyClass *selectClass; //MyClass是我写的班级类 ifstream readFile; // 读取文件流 Student Create(vector\u0026lt;string\u0026gt; \u0026amp;list) { //由传入的list来创建Student对象并返回 //代码略，见原代码 } void ImportCSV() { readFile.open(path + \u0026#34;\\\\\u0026#34; + files[u.index], ios::in); selectClass-\u0026gt;children.Clear(); string temp; getline(readFile, temp, \u0026#39;,\u0026#39;); getline(readFile, temp, \u0026#39;,\u0026#39;); readFile.get(); vector\u0026lt;string\u0026gt; list; int num = atoi(temp.c_str()); for (int i = 0; i \u0026lt; 10; i++) { getline(readFile, temp, \u0026#39;,\u0026#39;); } //用来忽略表头的 for (int i = 0; i \u0026lt; num; i++) { list.clear(); readFile.get(); //吞上一行回车 for (int j = 0; j \u0026lt; 10; j++) { getline(readFile, temp, \u0026#39;,\u0026#39;); list.push_back(p.assign(temp)); } selectClass-\u0026gt;children.Insert(Create(list), selectClass-\u0026gt;children.GetLength()); } readFile.close(); } CSV文件其实就是数值1,数值2,...这样组成的文件，这种文件格式可以被Excel读取\n输出的时候先输出基本班级信息，然后按行输出每个人的信息\n导入CSV时先读取基本班级信息，然后按行导入学生信息\n输出一般不大会遇到问题，就是读取时文件指针的控制是个问题\n这里在读取时用getline(ifstream,string,char)来读取，碰到,就截断\n前面还有readFile.get()来吞上一行的空格\n\u0026lt;0x04\u0026gt; 多文件管理 为了方便备份和多幼儿园管理(?)，实现了多工程文件的选择\n选择和访问都应该不成问题，但获取对应文件夹下指定类型的所有文件是个问题\n//关键代码 #include \u0026lt;io.h\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;string\u0026gt; using namespace std; void GetFiles(string path, vector\u0026lt;string\u0026gt; \u0026amp;files, string fileType) // 获取指定路径下指类型的所有文件名 { intptr_t hFile = 0; // 句柄编号 struct _finddata_t fileInfo; // 文件信息结构体 if ((hFile = _findfirst((path + \u0026#34;\\\\*\u0026#34; + fileType).c_str(), \u0026amp;fileInfo)) != -1) { do { files.push_back(fileInfo.name); // 给files加入文件名 } while (_findnext(hFile, \u0026amp;fileInfo) == 0); _findclose(hFile); // 关闭句柄 } } 这段代码实现了获取指定路径下指定文件类型的所有文件名\n通过句柄实现(也就是用系统api)\n最后的结果会保存在传入的files中\n注意，网上的多数代码会将句柄编号的类型写为long\n但在win10环境下，使用long类型的句柄会导致精度丢失进而导致错\n使用intptr_t就没这个问题\n\u0026lt;0x05\u0026gt; 从临时文件恢复 为了防止用户系统崩溃(也可能程序崩溃)导致的数据丢失\n管理系统在选择文件后会新建一个一样的同名的.tmp临时文件\n后续的所有文件写入操作都会对这个.tmp文件操作\n只有在用户正常退出系统时才会将.tmp文件保存为.dat文件\n具体来说就是选择文件时并没有真正打开选择的文件，而是读取到文件名\n在选择班级之后，正常读取文件\n如果在班级管理界面里修改了相关数据，并不是修改原文件，而是创建同名临时文件，将修改后的结果写入\n最后用户正常退出后，在主对象析构中把原文件删了，然后把临时文件的后缀改成.dat\n代码就不单独拆出来了，原代码里有具体标明的\n\u0026lt;0x06\u0026gt; 把所有东西拼起来 总体采用了UI与处理分离的设计\nUI.hpp负责所有的界面输出，ManageTool.hpp负责所有的数据处理和页面逻辑\nmain()函数里就建立了Tool对象然后调用MainDisplay()启动\n之后不同的功能就是不同的函数，不同的页面也是不同的函数\n页面函数调用下一个页面的函数来实现功能的切换\n页面函数相当于不同功能的调度器，页面输出还是靠UI.hpp的函数\nUI的选择结果由UIInfo传递\n其他的文件只是定义所需要的不同的类型\n这样就吧所有的东西都拼起来了\n后记 课设的基本要求都不难，但是都要完成\n如果要拿优秀，肯定是要扩充功能的\n如果想不到能扩充啥功能，可以优化UI\n课设一般也不大需要多少时间，熟练的话写完课设大概3天\n","date":"2023-06-15T23:42:16Z","permalink":"/post/articles/2023/%E6%B5%99%E5%B7%A5%E5%A4%A7c++%E8%AF%BE%E8%AE%BE%E8%AE%B0%E5%BD%95/","title":"浙工大c++课设讲解"},{"content":"\u0026lt;0x00\u0026gt; 为啥不用Cmake 怎么说呢Cmake确实不是很好用，虽然说我还是稍微懂点shell命令的，但用着就是感觉很怪\n虽然Cmake很强大，而且经过这么多年的发展，Cmake的社区支持也非常丰富\n但对于初学者，Cmake的各种函数还是显得略微晦涩难懂\n而且Cmake在VS Code的环境下报错代码会很日怪 (也可能是我没装好对应的插件，反正看报错挺费脑子的)\n\u0026lt;0x01\u0026gt; 为什么使用Xmake Xmake使用lua作为脚本语言，整体看着能更好理解，并且更加语句更少\n我常用的Cmake配置文件\n# CMakeLists.txt cmake_minimum_required(VERSION 3.0.0) project(Course VERSION 0.1.0) include(CTest) enable_testing() # 指定文件转编码为GBK set(CMAKE_CXX_FLAGS \u0026#34;${CMAKE_CXX_FLAGS} -Wall -fexec-charset=GBK\u0026#34;) # 指定编译器和标准 set(CMAKE_CXX_COMPILER \u0026#34;g++\u0026#34;) set(CMAKE_CXX_STANDARD 17) # 添加include路径 include_directories(include) aux_source_directory(src SRC) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) add_executable(${PROJECT_NAME} ${SRC}) 换成xmake.lua的等价配置文件\n--xmake.lua add_rules(\u0026#34;mode.debug\u0026#34;, \u0026#34;mode.release\u0026#34;) target(\u0026#34;xtest\u0026#34;) set_symbols(\u0026#34;debug\u0026#34;) --指定文件转编码为GBK add_cxxflags(\u0026#34;-fexec-charset=GBK\u0026#34;) set_kind(\u0026#34;binary\u0026#34;) --指定C++标准 set_languages(\u0026#34;c++17\u0026#34;) --添加inlude路径 add_includedirs(\u0026#34;include\u0026#34;) add_files(\u0026#34;src/*.cpp\u0026#34;) 是不是确实干净很多？而且更加易懂？\n所以说嘛，人不能折磨自己，如果刚刚打算跑C++的多文件编译，我强烈推荐用Xmake\n在文档中提到Xmake的构建效率可以达到ninja的水平，这个在我的使用中似乎并未体现\n(可能是我的项目相对比较小) 总之Xmake更现代，对很多东西都有很好的原生支持，具体可以看项目文档\n\u0026lt;0x02\u0026gt; 安装Xmake 如果有配置winget可以直接敲下面的命令\nwinget install xmake 也可以去 Github-xmake项目 下载安装包\n直接安装就可以了，全默认就可以\n\u0026lt;0x03\u0026gt; VS Code的配置 VS Code的配置也很简单，仅需安装一个叫Xmake的插件\n安装上就行了\n\u0026lt;0x04\u0026gt; 由Xmake创建项目 先新建个文件夹，作为存放项目代码的文件夹\n在VS Code中打开这个文件夹\n快捷键Ctrl+Shift+P打开命令窗口，输入Xmake\n选择XMake: CreateProject这个选项\n选择后一般右下角会有个窗口，提示没有 xmake.lua 文件，询问是否创建，创建就是了\n然后按你需要的选择配置，我通常选择 C++ 的命令行应用\n之后，项目的文件夹结构就创建好了\n里面的xmake.lua就是配置文件，里面已经写好了基本的配置\n如果有include的文件，创建个文件夹放然后在配置文件指明就好\n可以直接参考[\u0026lt;0x01\u0026gt;](#\u0026lt;0x01\u0026gt; 为什么使用Xmake)部分的配置文件\n之后创建新项目就按这个方法就可以了，轻松加愉快\n编译时按下面的Build或者▶就可以\n调试就是打完断点，然后按下面的🐞图标\n","date":"2023-06-02T19:45:28Z","permalink":"/post/articles/2023/%E4%BB%8Ecmake%E6%8D%A2%E5%88%B0xmake/","title":"从Cmake换到Xmake"},{"content":"搭个博客嘛，肯定要加点个人的一些账号。然后呢，我想加个B站的链接，但是在 Anatolo 的模板里没有，这下只能自己动手了。\n一些约定的符号 ./ 表示 hexo 博客的根目录\n\u0026lt;0x00\u0026gt; 分析 我先是在 ./themes/Anatolo/source/images 里找，发现里面只有网站的 icon 和主要图标，没有那些社交媒体的图标 (比如这样的图标，B站的图标原来是没有的)\n然后就又去找 ./themes/Anatolo/layout/partial 下面的各种文件，发现有个叫 sidebar.pug 和 social_links.pug 这两个文件。问题来了，.pub 是啥文件啊(之前真没接触过)。百度了以下，这是个排版引擎的语言。行吧，直接在线学习。\n然后打开了 sidebar.pug 简单分析下，感觉关系不大。打开 social_links.pug\n// social_links.pug\rul.social-links\rif theme.github\rli\ra(href= theme.github)\ri.fa.fa-github\rif theme.mail\rli\r//- a(href= \u0026#34;javascript:alert(\\\u0026#34;\u0026#34; + theme.mail +\u0026#34;\\\u0026#34;)\u0026#34; )\ra(href= \u0026#34;mailto:\u0026#34; + theme.mail )\ri.fa.fa-envelope\rif theme.QQ\rli\ra(href= \u0026#34;http://sighttp.qq.com/authd?IDKEY=\u0026#34; + theme.QQ)\ri.fa.fa-qq\rif theme.zhihu\rli\ra(href= theme.zhihu)\ri.fa.fa-mortar-board\rif theme.twitter\rli\ra(href= theme.twitter)\ri.fa.fa-twitter\rif theme.instagram\rli\ra(href= theme.instagram)\ri.fa.fa-instagram\rif theme.rss\rli\ra(href= url_for(theme.rss))\ri.fa.fa-rss\rif theme.weibo\rli\ra(href= theme.weibo)\ri.fa.fa-weibo\rif theme.facebook\rli\ra(href= theme.facebook)\ri.fa.fa-facebook 感觉很有关系\n\u0026lt;0x01\u0026gt; 简单的尝试 于是我就直接很粗暴的在 social_links.pug 里加上这个\nif theme.bilibili\rli\ra(href= theme.bilibili)\ri.fa.fa-bilibili 还有在 ./themes/Anatolo/_config.yml 里加上这个\nbilibili: https://space.bilibili.com/13629146 一编译，一部署，一看，果不其然，直接是空白。 然后看看开发者工具，发现在对应地方的 html 代码里多了这些\n\u0026lt;li\u0026gt; \u0026lt;a target=\u0026#34;_blank\u0026#34; rel=\u0026#34;noopener\u0026#34; href=\u0026#34;https://space.bilibili.com/13629146\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;fa fa-bilibili\u0026#34;\u0026gt; \u0026lt;/i\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; 也就是说，其实添加是添加进去了，但就是没有对应的图标而已\n问题来了，\u0026lt;i class=\u0026quot;fa fa-bilibili\u0026quot;\u0026gt; 里面的 fa 指的是什么呢?\n\u0026lt;0x02\u0026gt; 二度分析 搜索了一下，fa 指的是 Font awesome，是有各种常见图标的字体。找的官网，看了看图标列表，果然没有B站\n这下不得不自己做字体了(如果直接插入图片会更简单倒是)\n翻了翻 ./themes/Anatolo/source/fonts 果然有 FontAwesome 的字体文件\n(有 .otf .eot .svg .ttf .woff .woff2)\n\u0026lt;0x03\u0026gt; 制作图标 首先需要一个工具网站，用来做字体。然后还需要一个矢量图编辑器。\n网站是 icomoon.io 直接点击右上角的 IcoMoon App 即可\n矢量图编辑器随意，我这里用的是 inkscape\n进入 IcoMoon App 后，点击左上角 Import Icons，选择 .svg 文件\n左上角选择铅笔，然后随便选一个图标，点击 Download(SVG) 下载这个图标的 .svg 文件。\n然后在本地用矢量图编辑器打开，把原来的图清掉，然后再在上面画矢量图就可以了。\n画的时候注意不要用描边，要用填充来构成图形。如果不小心用描边构成了图形，那么也可以选中路径，然后找到这个\n点一下就会自动转换成填充了\n画的时候要尽可能把画布填满，如果上下留白很多的话可以适当减小。\n画布大小可以在文件→文档属性里修改\n如果贝塞尔曲线画不好的话可以先用有理B样条来画，后者好画一点，但画出来就是比较圆润。 用贝塞尔曲线画的画，尽可能让每个节点的手柄要么水平，要么垂直，这样画会相对简单一点\n如果图标有一定的对称性，那么节点要对称，而且手柄的长度也要对称\n我的成果供大家参考。\n然后，保存文件，回到之前的网页。导入我们制作的 .svg 文件。\n然后选择右上角移动工具\n把我们的图标移到列表的最后一个。\n最后选择 Generate Font 生成字体文件。下载前找到我们新制作的图标，可以先检查下有没有什么问题。\n\u0026lt;0x04\u0026gt; 替换原有字体 下载后解压缩，里面有个 demo.html 可以用来查看字体和字体编号(字体编号很重要)，还有 fonts 文件夹，里面就是我们生成的字体了。\n打开 demo.html，找到我们的图标，记下图标的编号(这个编号在字体生成里也能改)，比如我的字体编号是 f2e1。\n把 fonts 文件下的文件复制到 ./themes/Anatolo/source/fonts\n在 ./themes/Anatolo/source/css 里找到 font-awesome.css 和 font-awesome.min.css (建议每个复制一个作备份)，打开。\n在 font-awesome.css 修改\n/*原来的*/ @font-face { font-family: \u0026#39;FontAwesome\u0026#39;; src: url(\u0026#39;../fonts/fontawesome-webfont.eot?v=4.7.0\u0026#39;); src: url(\u0026#39;../fonts/fontawesome-webfont.eot?#iefix\u0026amp;v=4.7.0\u0026#39;) format(\u0026#39;embedded-opentype\u0026#39;), url(\u0026#39;../fonts/fontawesome-webfont.woff2?v=4.7.0\u0026#39;) format(\u0026#39;woff2\u0026#39;), url(\u0026#39;../fonts/fontawesome-webfont.woff?v=4.7.0\u0026#39;) format(\u0026#39;woff\u0026#39;), url(\u0026#39;../fonts/fontawesome-webfont.ttf?v=4.7.0\u0026#39;) format(\u0026#39;truetype\u0026#39;), url(\u0026#39;../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular\u0026#39;) format(\u0026#39;svg\u0026#39;); font-weight: normal; font-style: normal; } /*修改为*/ @font-face { font-family: \u0026#39;FontAwesome\u0026#39;; src: url(\u0026#39;../fonts/fontawesome-webfont.eot\u0026#39;); src: url(\u0026#39;../fonts/fontawesome-webfont.eot\u0026#39;) format(\u0026#39;embedded-opentype\u0026#39;), url(\u0026#39;../fonts/fontawesome-webfont.woff\u0026#39;) format(\u0026#39;woff\u0026#39;), url(\u0026#39;../fonts/fontawesome-webfont.ttf\u0026#39;) format(\u0026#39;truetype\u0026#39;), url(\u0026#39;../fonts/fontawesome-webfont.svg\u0026#39;) format(\u0026#39;svg\u0026#39;); font-weight: normal; font-style: normal; } 我这里的字体名还是 fontawesome-webfont 这里名字按自己的来\n然后加上这些\n.fa-bilibili-alt:before { content: \u0026#34;\\f2e1\u0026#34;; } 反正有很多长差不多的，加在那里面就可以。\n\\f2e1 这里要看你的编号填，.fa-bilibili-alt 这个算是给图标起个名字，随意就好。\nfont-awesome.min.css 就是 font-awesome.css 去掉所有缩进的版本，也要加上并修改些东西，在这里就不讲了。\n\u0026lt;0x05\u0026gt; 部署试试 (记得加上\u0026lt;0x02\u0026gt;的多出来的代码)\n编译，部署。访问下网页看看有没有新图标出现。如果没有，清除或禁用网页缓存后再试试。理论上自己加入的图标就会出现。\n就像这样。\n","date":"2023-04-26T21:59:57Z","permalink":"/post/categories/blog/%E7%BB%99%E4%BE%A7%E8%BE%B9%E6%A0%8F%E5%8A%A0%E4%B8%AAb%E7%AB%99%E5%9B%BE%E6%A0%87/","title":"给侧边栏加个B站图标"},{"content":"用于HP Prime的绘图软件 (@ 24-02-09)\n原本的服务器已经不续费了，现在只有Github的了\ngithub链接\n使用教程(B站)\n之后应该不更新了，手头没机子测试了\n","date":"2023-04-26T21:53:36Z","permalink":"/post/articles/2023/hp-primepaint/","title":"HP-PrimePaint"},{"content":"\u0026lt;0x01\u0026gt; 如何实现 思路是本地多写几份 config 文件\n在部署时切换就可以\n\u0026lt;0x02\u0026gt; 开搞 首先先新建几个 config 文件\n建议直接复制原来的_config.yml\n复制的文件名字随意 我们只需要修改最下面的deploy部分就好\n比如我在 hexo 目录建了两个文件\n分别是_GitPageConfig.yml与_ServerConfig.yml\n# _GitPageConfig.yml的部分 deploy: type: git repo: git@github.com:{github用户名}/{github用户名}.github.io.git branch: gh-pages # _ServerConfig.yml的部分 deploy: type: git repo: git@{服务器ip}:{gitea用户名}/{gitea仓库名}.git branch: master 然后新建一个脚本文件\n这里用 windows 配置，所以新建了个 powershell 脚本\n# deploy.ps1 copy _ServerConfig.yml _config.yml hexo g -d del _config.yml copy _GitPageConfig.yml _config.yml hexo g -d del _config.yml 如果要部署到更多的git仓库，以此类推 之后部署只需要执行deploy.ps1即可\n\u0026lt;0x03\u0026gt; 为什么不单文件解决 (@ 23-07-07)\n经过我的实际测试，确实也可以直接单文件搞定\n# _config.yml deploy: - type: git repo: git@{服务器ip}:{gitea用户名}/{gitea仓库名}.git branch: master - type: git repo: git@github.com:{github用户名}/{github用户名}.github.io.git branch: gh-pages 这样写也可以，hexo会按从上到下的顺序依次部署\n但这样写会导致一个问题，就是网址的根域名会对不上\n# _config.yml # 部署到我自己的网站 url: http://mlacookie.top/ # 部署到GitHubPage url: https://MLAcookie.github.io/ 如果仅靠单一文件的话，两个部署的根域名会是一样的\n这样可能会造成一些小问题\n靠powershell还可以顺便做部署前的一些操作，挺方便的\n","date":"2023-04-23T16:35:19Z","permalink":"/post/categories/blog/%E5%A6%82%E4%BD%95%E4%B8%80%E4%B8%AAhexo%E5%8D%9A%E5%AE%A2%E5%90%8C%E6%97%B6%E9%83%A8%E7%BD%B2%E5%A4%9Agit/","title":"如何一个hexo博客同时部署多git"},{"content":"\u0026lt;0x00\u0026gt; 为什么要用gitea + hexo 很简单，gitea 太好用了(才不是因为 git 配置太麻烦)\n而且 hexo 的博客就是静态网页，基本不怎么耗费服务器资源\n如此再搭个 gitea 正好还能当个代码仓库，岂不美哉\n\u0026lt;0x01\u0026gt; 整体运行架构 就是 gitea 跑 git 仓库\n然后利用 git hook 功能将仓库内容同步到服务器的另一个文件夹\nhexo 的界面靠 nginx 反代 本地写完靠 hexo 的配置脚本通过 git 同步到服务器\n(其实用 gitea 的话也可以通过插件实现 GitHub Page 的功能的)\n\u0026lt;0x02\u0026gt; 先安装些组件 #安装nginx sudo apt update \u0026amp;\u0026amp; sudo apt upgrade sudo apt install nginx gitea 的安装在这就不赘述了\n(我之前有从零搭建的教程)\n\u0026lt;0x03\u0026gt; 配置git hook git hook 其实就是一个自动化脚本\n简单理解就是 git 到服务器上后服务器要干的事\n首先，在 gitea 里创建一个仓库，名字随意\n#进入对应仓库的hook配置目录 cd /var/lib/gitea/data/gitea-repositories/${gitea用户名}/${仓库名}.git/hooks/ (${gitea用户名}和${仓库名}都是按照自己的设置填的) 这里是 git 的各种 hook，别的现在用不大到\n#进入需要编辑的hook目录 cd post-receive.d/ #创建我们自己的hook，名字随意 vim hexo 在 hexo 文件中，输入以下内容\n#!/bin/bash sss=/var/lib/gitea/data/gitea-repositories/${gitea用户名}/${仓库名}.git ttt=/var/www/blog rm -rf ${ttt} git clone ${sss} ${ttt} 其中/var/www/blog是你希望存储网页的地址，可以改成别的\n#给这个hook添加可执行权限 chmod a+x hexo 现在，理论上当你用 hexo 一键配置脚本的时候，那些网页文件会出现在那个位置\n\u0026lt;0x04\u0026gt; 配置存储地址和nginx #进入你希望存网页的位置 cd /var/www/ mkdir blog #为第一次git用户删除提供权限 chmod -R 777 ./ #进入nginx的目录 cd /etc/nginx/ cd sites-enabled/ vim default 需要注意的是，这里的nginx版本可能比较新，所以需要修改/etc/nginx/sites-enabled/default\n如果你在/etc/nginx/nginx.conf中能看到在http{}中有server{}\n那么直接在/etc/nginx/nginx.conf就行\n在server{}中，把root的值改为存储网页的位置\n在这里是改为root /var/www/blog 再在index后加入index.html (有了就不用加)\n#重启nginx服务 nginx -s reload 现在，理论上访问服务器 ip 后就是博客首页了\n\u0026lt;0x05\u0026gt; 本地的一些修改 打开_config.yml\ndeploy: type: git repo: git@{服务器ip}:{gitea用户名}/{gitea仓库名}.git branch: master 现在，当你运行hexo deploy，网页内容将同步到git仓库并且也会同步到网页存储位置\n","date":"2023-04-23T15:50:19Z","permalink":"/post/categories/blog/%E6%90%AD%E5%BB%BAhexo+gitea%E7%9A%84%E5%8D%9A%E5%AE%A2%E7%AB%99/","title":"搭建hexo+gitea的博客站"},{"content":"找到gitea的配置文件\n一般在/etc/gitea中(如果按官方文档配置的话)\n#打开gitea配置文件 cd /etc/gitea vim app.ini 然后往下翻 找到 DISABLE_REGISTRATION\n把 false 改成 true 就可以了\n然后记得重启gitea服务\n#如果已经配置了linux服务 systemctl restart gitea ","date":"2023-04-23T15:40:43Z","permalink":"/post/articles/2023/%E5%85%B3%E9%97%ADgitea%E6%B3%A8%E5%86%8C%E5%8A%9F%E8%83%BD/","title":"关闭gitea注册功能"},{"content":"\u0026lt;0x00\u0026gt; 安装组件 首先肯定要有台服务器嘛，系统倒问题不大，我用的是debian 11\n# 安装各种包前先更新是好文明 sudo apt update sudo apt upgrade # 安装git sudo apt install git # 安装mysql wget https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb sudo dpkg -i mysql-apt-config_0.8.22-1_all.deb # 或者安装postgresql sudo apt install postgresql # 这里可能会跳出个界面，直接选最后的ok就可以 rm mysql-apt-config_0.8.22-1_all.deb sudo apt update sudo apt-get install mysql-server 安装mysql的时候会要求你设置数据库 root 账户密码的\n\u0026lt;0x01\u0026gt; 配置数据库 mysql 在/etc/mysql/mysql.conf.d/mysqld.cnf中加入bind-address = 127.0.0.1 这个配置文件的位置看版本，我这里的 mysql 版本是8.0.33 这个bind-address是控制 mysql 数据库允许远程访问的\n#先进入mysql控制台 mysql -u root -p` #输入你设置的root账户密码 #配置gitea的数据库 CREATE USER \u0026#39;gitea\u0026#39; IDENTIFIED BY \u0026#39;密码\u0026#39;; #这里写的‘密码’是你想设置的密码 CREATE DATABASE giteadb CHARACTER SET \u0026#39;utf8mb4\u0026#39; COLLATE \u0026#39;utf8mb4_unicode_ci\u0026#39;; GRANT ALL PRIVILEGES ON giteadb.* TO \u0026#39;gitea\u0026#39;; FLUSH PRIVILEGES; exit postgresql # 进入postgre控制台 sudo -u postgres psql # 密码尽量改一下 CREATE ROLE gitea WITH LOGIN PASSWORD \u0026#39;gitea\u0026#39;; # 创建数据库 CREATE DATABASE giteadb WITH OWNER gitea TEMPLATE template0 ENCODING UTF8 LC_COLLATE \u0026#39;en_US.UTF-8\u0026#39; LC_CTYPE \u0026#39;en_US.UTF-8\u0026#39;; \u0026lt;0x02\u0026gt; 配置 gitea 和 git 账户 #下载git安装包 wget -O gitea https://dl.gitea.com/gitea/1.19.0/gitea-1.19.0-linux-amd64 chmod +x gitea #添加git用户并修改权限 adduser git mkdir -p /var/lib/gitea/{custom,data,log} chown -R git:git /var/lib/gitea/ chmod -R 750 /var/lib/gitea/ mkdir /etc/gitea chown root:git /etc/gitea chmod 770 /etc/gitea 手动将下列语句加入/etc/profile文件 export GITEA_WORK_DIR=/var/lib/gitea/ (导出个全局变量)\n#一些剩下的操作 cp gitea /usr/local/bin/gitea sudo vim /etc/systemd/system/gitea.service #将gitea.service的内容复制进去，并取消一些服务的注释，如mysql的 sudo systemctl enable gitea sudo systemctl start gitea 现在访问(你的ip):3000应该是能看到gitea的网页了\n\u0026lt;0x03\u0026gt; 网页配置 在网页的安装界面，你需要设置你的数据库用户和用户密码\n然后指定数据库名字\n#本次配置的值 数据库用户：gitea 用户密码：(之前给gitea设置的密码) 数据库名称：giteadb 记得在网页最后那里设置管理员账户\n(不在这配置也行，理论上第一个创建的账户就是管理员)\n创建新用户后记得绑定 sshkey，这样就可以用 ssh 的 clone 地址了\n\u0026lt;0x04\u0026gt; 收尾配置 #最后记得把一些权限修改回来 chmod 750 /etc/gitea chmod 640 /etc/gitea/app.ini 附录：git.service 用什么数据库就取消注释哪一行\n# git.service [Unit] Description=Gitea (Git with a cup of tea) After=syslog.target After=network.target ### # Don\u0026#39;t forget to add the database service dependencies ### # #Wants=mysql.service #After=mysql.service # #Wants=mariadb.service #After=mariadb.service # #Wants=postgresql.service #After=postgresql.service # #Wants=memcached.service #After=memcached.service # #Wants=redis.service #After=redis.service # ### # If using socket activation for main http/s ### # #After=gitea.main.socket #Requires=gitea.main.socket # ### # (You can also provide gitea an http fallback and/or ssh socket too) # # An example of /etc/systemd/system/gitea.main.socket ### ## ## [Unit] ## Description=Gitea Web Socket ## PartOf=gitea.service ## ## [Socket] ## Service=gitea.service ## ListenStream=\u0026lt;some_port\u0026gt; ## NoDelay=true ## ## [Install] ## WantedBy=sockets.target ## ### [Service] # Uncomment the next line if you have repos with lots of files and get a HTTP 500 error because of that # LimitNOFILE=524288:524288 RestartSec=2s Type=simple User=git Group=git WorkingDirectory=/var/lib/gitea/ # If using Unix socket: tells systemd to create the /run/gitea folder, which will contain the gitea.sock file # (manually creating /run/gitea doesn\u0026#39;t work, because it would not persist across reboots) #RuntimeDirectory=gitea ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini Restart=always Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea # If you install Git to directory prefix other than default PATH (which happens # for example if you install other versions of Git side-to-side with # distribution version), uncomment below line and add that prefix to PATH # Don\u0026#39;t forget to place git-lfs binary on the PATH below if you want to enable # Git LFS support #Environment=PATH=/path/to/git/bin:/bin:/sbin:/usr/bin:/usr/sbin # If you want to bind Gitea to a port below 1024, uncomment # the two values below, or use socket activation to pass Gitea its ports as above ### #CapabilityBoundingSet=CAP_NET_BIND_SERVICE #AmbientCapabilities=CAP_NET_BIND_SERVICE ### # In some cases, when using CapabilityBoundingSet and AmbientCapabilities option, you may want to # set the following value to false to allow capabilities to be applied on gitea process. The following # value if set to true sandboxes gitea service and prevent any processes from running with privileges # in the host user namespace. ### #PrivateUsers=false ### [Install] WantedBy=multi-user.target 从gitea文档扒的\n","date":"2023-04-20T23:05:44Z","permalink":"/post/articles/2023/%E4%BB%8E0%E6%90%AD%E5%BB%BAgitea/","title":"从零搭建一个gitea"}]