ImageUpdateTool 开发经历

ImageUpdateTool 开发经历

Github仓库链接

背景

有时候我们需要在博客中插入图片,这个时候我们需要利用图床来存储这些在线的图片,此时通常有三种方式:

  1. 直接花钱购买,例如阿里云OOS存储服务器,七牛云等。一般新用户会有一些免费的额度可以白嫖。

  2. 如果自己部署博客有自行部署/购买服务器,那可以用自己的服务器做个图床出来。

  3. 用Github仓库作图床。(纯白嫖,不用花钱,缺点是国内访问速度慢且不稳定)

这里我用的Github仓库做的图床。最开始我也是使用的PicGo作为上传图片的工具,但是在使用了一段时间后感觉到了几个问题:

  1. 应该是网络的问题,上传成功率没有保障。经常拖进去图片之后等半天没反应,然后反复托进去图片,过很久可能一下子传上去好几个。

  2. 图片都在根目录下面,不便于管理。我想在上传的时候根据日期建立目录。

  3. 不方便查看过去上传的全部图片,只能看最近上传的一部分。时间久了以前上传的图就看不到了。这样如果我再写文章需要用到过去的图,想要获取URL链接就会比较麻烦。

当然以上问题可能是因为我PciGo用的不专业。但我还是决定自己做个软件实现我的需求。

技术选型

我思考了一下我的需求和使用场景,总结如下:

  1. 我基本上没有跨平台的需求,我只有windows的设备。但是支持跨平台的话,也许以后可以继续拓展这个软件,例如以后在手机上看到喜欢的图可以快速传到图床上存下来,或者以后可能买Mac做开发等。

  2. 最好可以有较快的启动速度和较低的内存占用,使得我能够将这个软件一直挂在后台。

  3. 界面尽可能美观一点。

而我“会”的开发框架如下:

  1. Wpf

  2. MAUI

  3. Unity

  4. Qt

  5. Electron

注:玩过 = 会 🤣 其实这几个我都只停留在做过一些简单的项目的层次上,比如图书管理系统,画板,扫雷,贪吃蛇等

其中wpf没有跨平台能力,基本上可以被MAUI替代。Electron内存占用比较大,启动速度也比较慢(我肯定还不具备什么优化能力)。Unity的话是一个备选方案,有一说一Unity的UGUI非常好用,做一些小软件开发其实非常快的。但是它的逻辑是游戏的那一套思路,比如一直在不停的刷新,那可能对笔记本来说耗电会比较多,我不确定常驻后台是不是影响有点大,所以没选择Unity。Qt则是我三年前学的东西了,好久没用是一个方面,C++本身开发速度估计也比较慢的,想了想放弃了。

最后决定就是MAUI了,这是一个很新的框架,我感觉我这个软件涉及的内容也不是特别复杂,应该不会遇到特别多奇怪的坑。(当然还是踩了好久的坑)

简单介绍MAUI

什么是MAUI?

这是微软开发并维护的新一代跨平台UI框架(跨平台:Linux??)。

其使用Xaml文件去定义静态的UI,使用C#代码完成动态的部分以及其功能(类似html与js)。同时Style也在Xaml中定义。

开工?坑?

在完成了Microsoft Learn中的六个MAUI入门教程后,我就开始了软件的开发工作。

怎么获取路径

我遇到的第一个“坑”是路径问题。

最开始我找了Environment这个静态类,从中可以获取一些路径信息,例如:

1
2
3
4
5
Environment.CurrentDirectory // C:\WINDOWS\system32
Environment.ProcessPath // 项目所在路径\ImageUpdateTool\bin\Debug\net7.0-windows10.0.19041.0\win10-x64\AppX\ImageUpdateTool.exe
Environment.SystemDirectory // C:\WINDOWS\system32
Environment.GetFolderPath(SpecialFolder folder, SpecialFolderOption option)
// 根据SpecialFolder这个枚举,可以获取文档、桌面、ProgramFiles等特殊文件夹的路径

当我想要在C:\User\{UserName}\AppData\之中做个文件夹存放软件的数据时,我使用SpecialFolder.LocalApplicationDataSpecialFolder.ApplicationData分别获取到了Local和Roaming这两个文件夹的路径。但当我尝试去创建文件夹时,我发现根本就没创建出来任何文件夹。经过一会研究后,我发现是因为MAUI会将这些路径重定向到\Local\Packages\{GUID}\LocalCache\Local\Packages\{GUID}\LocalState这两个文件夹中。并且通过FileSystem.CacheDirectoryFileSystem.AppDataDirectory进行访问。

最开始我对此感觉比较迷惑,因为这对于用户来说想要查看应用的一些数据太过于麻烦了。GUID是很长的一串十六进制数,并且Packages文件夹下面有很多这样的子目录,那我怎么记得住每个个程序的GUID是啥。不过后来在 Stackoverflow 上与大佬们交流了一下之后,我能够从两点理解,其一是不同的程序可能有相同的名字,但不可能有相同的GUID;其二是微软在这方面的设计就是这样子,不希望用户直接去访问应用的这些数据。如果有访问的需要,也应该是应用程序提供一个访问/编辑的界面。

关于新老文件系统思路的探讨

这里插播一个题外话,可能是因为从小在Windows上玩游戏和使用破解软件的经历(比如经常需要直接将应用程序目录下的一部分文件用另一部分破解文件替换掉),使得我内心惯性的认为能够去访问这些文件,才是合理的。所以我所意识到的文件系统就类似这样子:每个应用分三部分存储,应用本体,用户数据,系统注册项。其中应用本体我们可以自己选择安装的位置,默认是Program Files,你也可以改到D盘等地方。用户数据则是操作系统不同用户使用该程序所产生的相互独立的数据,被分别存储在每个用户的AppData中。如果仅想为某个用户安装应用时,则将应用本体和用户数据放在一起,均在AppData下。系统注册项则是一个键值对,用于告诉操作系统有这个应用的存在。

Windows中提供了Pictures、Videos、Musics、Documents等文件夹,其目的我想就是希望可以用一个统一的方式去访问机器中的某一类型文件,这一点在手机上非常常见。但是很显然这个方式到目前为止还不成熟,我举个很简单的例子,我在微信上下载了一个文件,我从QQ上想要将其分享给其他人,我从文档中并看不到微信下载的内容。最后我还是需要一点点的去扒微信存储文档的目录,才能找到文件。在Windows上这个操作很容易,因为我安装微信的时候,我自己知道我把微信装在哪里了,我知道下载的内容去FileRecv这个文件夹查看。但是在手机中,我根本不知道微信被装到哪里了。所以最后我一般会选择用office打开那个文档,另存为到手机根目录下,这样我可以很容易的找到这个文档。

这种文件管理方式,对于移动端用户来说是方便了,也更容易让小白上手(毕竟对于一个不太懂计算机的人来说,你让他选择软件装在哪里,他都不知道该如何操作)。但也确实存在一些未解决的问题,包括软件的多开,比如在Windows上我可以登录10个qq,但是手机上最多双开。比如上面讲的文件在多个App中传递的问题,我也是经常被身边的人问到。Windows为MAUI采取这样的管理方式,也许是一种UWP的遗留,也许是为了跨平台而考虑的。不好说这两种文件管理方式哪个更优,只能说我更习惯用传统Windows的管理思路。

以上仅为个人胡思乱想,可能存在错误

查看是否安装Git

因为本软件的运行是依赖Git的,所以在运行程序之前,要先检测一下机器中是否已经安装了Git。因为当时我注册了一个ChatGPT的账号,所以这里我直接问的ChatGPT 🤣。

  1. 检测注册表
1
2
3
4
string gitKey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Git_is1";
Microsoft.Win32.RegistryKey key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(gitKey);

if (key == null) // 未安装Git
  1. 检测环境变量
1
2
string path = Environment.GetEnvironmentVariable("Path");
if (!path.Contains("Git\\cmd")) // 未安装Git

通过注册表,我们可以确定是否安装了Git,但是其实我们并不知道Git的安装路径(虽然默认路径是C:\Program Files\Git,但显然安装时我们是可以修改它的)。不过如果用户已经将该路径写入环境变量了,我们就一定可以仅通过”git”指令就能调用到git程序。所以其实仅需在程序中检测一下环境变量中存不存在”Git\\cmd”这个路径就ok了。

这里有个小坑,在Windows下,‘\\’‘/’均可以分割路径(默认是‘\\’),所以如果用户自己手动在环境变量中敲入”C:/Program Files/Git/cmd”那我们的Contains("Git\\cmd")是无法匹配到这里的”Git/cmd”的,所以最好写两遍,把这两个都匹配一下,能匹配到一个就说明安装好了Git。

不过问题不大,Git的安装程序是会自己将自己写入环境变量的,并且是用的Windows的反斜杠‘\\’。

TreeView

MAUI中的控件都比较基础,当然啦通过这些基础控件我们可以进行无数的组合。但是有些时候进行组合这个工作也是有一定难度的(自己造轮子)。所以我这里直接引入了Uraniumui库。这个库不仅提供了一整套比较美观的颜色主题,为每个控件设计了各种状态下的配色;还额外开发了几个常用的控件,例如TreeView,Divider等。

不过Uraniumui中的TreeView的可定制性仍然不够强大,例如仅能点击”>”按钮进行展开或折叠,触发区域比较小。但是我们可以在他的Item部分做文章,来实现我们的各种点击效果。所以它仍然是足够优秀的。

Button的文本限制太死了

在这个阶段我遇到的困扰我最久的问题是MAUI中Button文本只能居中显示,具体如下图所示:

四种TreeView

在图1中,可以看到文字和Icon是居中显示的,但是在TreeView中居中展示文本不整齐不好看。在图2中,虽然Icon和文字左对齐了,但是点击区域却只能跟着文本长度走,标题短,那点击区域就小;标题长,点击区域就长。这也不是我想要的,(注:这里Icon和文本在每个Button中其实还是居中对齐,只不过没有限定Button的宽度)。图3则是VScode这个软件的TreeView效果,可以看到整个横行都可以点击,看上去也很美观。图4则是经过我一番折腾实现的效果,实现了与VScode类似的样式。

MAUI中的UI逻辑与我想象中的不同,所以在我发现官方没有提供为Button修改文字对齐的属性时,下意识的想的就是自己做一个Button出来。因为在UGUI中,想Button这样的组件其实原理就是若干原始组件的组合,比如一个触发器接收各种点击事件,一个SpriteRender展示背景,一个Label显示文字等等。可能MAUI也是类似的思路,但是在自定义上我还没找到切入点,对着源码看了一段时间自己尝试去写一个public class TreeViewButton : Label,让其实现一些接口来获得点击功能。当然最后试了半天也没成功。

最后采取的思路是控件叠加,我在下面放一个没有文本的Button,再从其上面放一个Label,大功告成 🤣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<material:TreeView x:Name="FolderTree" Spacing="5" Padding="5">
<material:TreeView.ItemTemplate>
<DataTemplate>
<Grid RowDefinitions="20"
ColumnDefinitions="25, 30, 141"
HorizontalOptions="Fill"
Margin="-19, 0, 0, 0">
<!--这个-19是让Item部分向左偏移一部分距离,让Button的点击范围盖住'>'按钮,使得一行看上去是一个整体-->
<Label x:Name="FolderIcon" Text="📁"
Margin="0"
Grid.Row="0" Grid.Column="1"/>
<Label Text="{Binding Name}"
FontAttributes="Bold"
VerticalOptions="Start"
Grid.Row="0" Grid.Column="2"/>
<!--为了让每个Button的点击区域的右边界相同,需要在初始化Item的时候手动计算并设置ButtonWidthRequest-->
<Button HorizontalOptions="Start"
Padding="0"
StyleClass="TextButton"
HeightRequest="20"
WidthRequest="{Binding ButtonWidthRequest}"
CornerRadius="3"
Clicked="FolderButton_Clicked"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
/>
</Grid>
</DataTemplate>
</material:TreeView.ItemTemplate>

这个方案下,点击区间的左端会随着树形图层级而缩进,但是右端保持对齐,这样更有种层级感。

点击区域随层级缩进

如果想要做出和VScode那样子点击区域从头贯穿到尾的效果,只需要在Grid的Margin属性下手就好了,每次计算一下要向左偏移多少。

不过我这个还是做的不够好,ButtonWidthRequest这个属性,默认宽度是200,然后根据Item的层级深度,每深一层减少10长度。这样子有两个坏处,一是屏幕比较大的时候,我也只有宽为200的点击区域,不会自适应的变大;二是当我把窗口缩小时,其实点击范围的宽度不会跟着变小,而仅仅是因为TreeView所在的Grid列宽度缩小,展示不全了,效果就是左侧是有圆角的,右侧变成直角了(被截断了)。

左右两侧不同了

注:目前想到的方案是,让TreeView也关注页面的SizeChanged的事件,在发生SizeChanged时,重新计算一下每个Button的宽度。但是我认为这个方案性能很差,不如这样子:取消Button的圆角,然后默认长度设置的更长一些,这样子就看不出来被截断了,还不影响性能。然后就是可能有人会想到直接让Button的Horizontal属性是Fill不就行了,这个我尝试过,因为Button的Text是空的,所以即便设置了Fill,依然只有一点点大小,不能填充全部。

设置Fill属性也不行

后续要解决的问题

异步

第一次进入软件,配置好设置后,点击Apply。此时软件就会卡住,这自然是因为程序正在后台clone仓库。因为这个流程是通过Process.Start去开启一个git线程,然后我直接使用WaitForExit(),所以这个时候前台UI线程就被阻塞了。后续会采用异步的方式,在前台显示一个进度条,或者“加载”样式的图标,告诉用户现在后台正在执行程序,请耐心等待。

同样的,点击Select Image后,软件也会卡住一会,这里也需要做同样的异步处理。

更方便的使用

现在要想上传一个图片,需要点击Select Image,在MediaPicker窗口中选择图片。显然这仍然不够方便,如果我可以直接拖一个图片进来,或者直接将剪切板的图片复制过来就上传,那才叫方便。

更方便的查看图片

目前来说,在软件中确实可以查看所有图片的缩略图。但是缩略图未免有点太小了(固定了150×120的尺寸),后续应当加一个点击图片就可以放大详细查看一个图片。同时虽然我提供了一个Open Repository Folder按钮来打开仓库文件夹,但是如果想要在文件资源管理器中找到某个特定的图片,还是需要深入点击多层文件夹,后续应当为图片提供一个“在文件资源管理器中打开”的选项,也许可以为图片做一个右键菜单来加入这个功能。

更方便的管理图片

显然,对于这个图床来说,目前是“只进不出”。只有上传图片的功能,缺少了移除图片的功能。这样如果我搞错了,传错了文件,仍需要手动去移除它。后续将会加入删除图片的功能,这个功能也许也可以放在右键菜单中。

更多细节

  1. 目前导航栏中浮出控件的Icon还都没做颜色主题的适配;

  2. 缺少语言切换功能,至少要支持中/英文切换;

  3. 直接为图片类型文件注册右键菜单,这样都不用打开软件界面,就能完成图片上传

  4. 在设置中添加可以允许开机自启功能

  5. ……

总结

虽然只是一个很简单的软件开发,但是在这个过程中也学到了很多东西。不过软件整体还不够优秀,比如架构上代码写的比较随意,后续应该系统学习一下MVVM,依赖注入等等内容。再接再厉吧。

文章作者: FcAYH
文章链接: http://www.fcayh.cn/2023/02/15/ImageUpdateTool-development-exprience/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Forever丶CIL