Loading... # 软件管理小工具开发实录(一) ## 一、背景介绍 ### 1.快捷访问。 因为本人爱好过于繁杂,日常需要打开的文件(夹)、软件太多,又过于零散,每次都需要去硬盘找半天。虽然借助everything搜索减轻了不少工作量,但是对于音速启动(vstart)类软件的需求,还是很强烈。 ### 2.小工具集成 能满足日常工作和业余爱好中机械式计算和转换的需求,最好能嵌入脚本进行定制计算。 ### 3.MQTT集成 最近对智能家具、物联网有点小兴趣,准备玩玩ESP8266之类的轻量级玩具。集成MQTT client,就可以订阅相关topic,进行实时掌控。 ## 二、技术选型 原本最佳的选择应该是Qt的,自带跨平台特性加丰富友好的基础库。 但是实际没有跨平台需求,同时对于MVVM之类的技术蛮感兴趣的,想深入研究下。 于是就选择了wpf。 ## 三、阶段性成果 大概花了2-3天的时间,把整体界面和基础框架搭好了,目前出来了个小玩意儿。 目前具备的功能: 不产生中间文件(快捷方式等文件) * 自定义类别 * 自动抽取文件(夹)默认ico图标及软件ico图标 * 配置文件读取和保存 * 拖拽方式添加文件(夹)和软件 * 支持使用默认浏览器打开网站 对于快捷访问,目前遇到的问题: * 目前所使用的MVVM框架为开源PRISM框架,其过于臃肿庞大,缺乏view和viewmodel之间的通信。导致手动删除、增加类别和项陷入困境。 * 简单弹出输入框与model之间的通信,还是需要使用MVVM的方式来展开,过于繁琐。 ## 四、具体细节 ### 1.UI层 采用开源MahApps.Metro进行界面绘制。 ```xml <mah:MetroWindow x:Class="SoftMa.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SoftMa" xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" xmlns:system="clr-namespace:System;assembly=mscorlib" xmlns:prism="http://prismlibrary.com/" xmlns:tb="http://www.hardcodet.net/taskbar" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" mc:Ignorable="d" NonActiveGlowBrush="#CDFF0000" Background="#FFFFFF" ShowIconOnTitleBar="true" ShowInTaskbar="False" Icon="logo.ico" prism:ViewModelLocator.AutoWireViewModel="True" Closing="MetroWindow_Closing" Visibility="{Binding MainWindowVisiable, Mode=TwoWay}" Title="{Binding SoftTitle}" Height="650" Width="270" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" IsMaxRestoreButtonEnabled="false" IsMinButtonEnabled="true"> <mah:MetroWindow.RightWindowCommands> <mah:WindowCommands ShowSeparators="False"> </mah:WindowCommands> </mah:MetroWindow.RightWindowCommands> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="48"/> </Grid.RowDefinitions> <!--主界面--> <TabControl x:Name="tabControl" ItemsSource="{Binding FileLists ,Mode=TwoWay}" TabStripPlacement="Left" mah:HeaderedControlHelper.HeaderFontSize="18" Background="#FFFFFF"> <!--类别--> <TabControl.ItemTemplate> <DataTemplate> <ContentPresenter Content="{Binding Head}"/> </DataTemplate> </TabControl.ItemTemplate> <!--软件、文件列表:软件、文件ico图标 | 软件、文件名--> <TabControl.ContentTemplate> <DataTemplate> <DataTemplate.Resources> <Style x:Key="itemstyle" TargetType="{x:Type ListViewItem}"> <EventSetter Event="MouseDoubleClick" Handler="HandleLeftClick"/> <Setter Property="ToolTip" Value="{Binding Path=FullfilePath ,Mode=TwoWay}" /> </Style> </DataTemplate.Resources> <ListView x:Name="listView" ItemsSource="{Binding FileInfo ,Mode=TwoWay}" ItemContainerStyle="{StaticResource itemstyle}" Drop="Event_File_Drop" AllowDrop="True" SelectionMode="Single" > <i:Interaction.Triggers > <i:EventTrigger EventName="Drop" > <prism:InvokeCommandAction Command="{Binding ClickRunCommnd}" CommandParameter="{Binding RelativeSource}" /> </i:EventTrigger> </i:Interaction.Triggers> <ListView.Resources> <DataTemplate x:Key="IconTemplate"> <Image Grid.Column="0" Source="{Binding Ico,Mode=TwoWay}" Width="16" Height="16"/> </DataTemplate> </ListView.Resources> <ListView.View> <GridView> <GridViewColumn CellTemplate="{StaticResource IconTemplate}" /> <GridViewColumn DisplayMemberBinding="{Binding FileName ,Mode=TwoWay}" Width="160"/> </GridView> </ListView.View> </ListView> </DataTemplate> </TabControl.ContentTemplate> </TabControl> <!--底部按钮块--> <StackPanel Grid.Row="1" Orientation="Horizontal"> <ToggleButton Width="40" HorizontalAlignment="Left" Height="40" Margin="3" Foreground="{DynamicResource MahApps.Brushes.Accent}" Style="{DynamicResource MahApps.Styles.ToggleButton.Circle}"> <ToggleButton.ContentTemplate> <DataTemplate> <iconPacks:PackIconModern Width="26" Height="26" Kind="Settings" /> </DataTemplate> </ToggleButton.ContentTemplate> </ToggleButton> <Button Width="40" HorizontalAlignment="Left" Height="40" Margin="3" Foreground="{DynamicResource MahApps.Brushes.Accent}" Click="BtnClick_AddBox" ToolTip="增加分组" Style="{DynamicResource MahApps.Styles.Button.MetroWindow.Light}"> <Button.ContentTemplate> <DataTemplate> <iconPacks:PackIconIonicons Kind="AddCircleMD" Width="26" Height="26"/> </DataTemplate> </Button.ContentTemplate> </Button> <Button Width="40" HorizontalAlignment="Left" Height="40" Margin="3" Foreground="{DynamicResource MahApps.Brushes.Accent}" Click="BtnClick_DelBox" ToolTip="删除分组" Style="{DynamicResource MahApps.Styles.Button.MetroWindow.Light}"> <Button.ContentTemplate> <DataTemplate> <iconPacks:PackIconIonicons Kind="RemoveCircleiOS" Width="26" Height="26"/> </DataTemplate> </Button.ContentTemplate> </Button> <TextBox Margin="3" mah:TextBoxHelper.ButtonCommand="{Binding TextBoxButtonCmd, Mode=OneWay}" HorizontalAlignment="Right" VerticalAlignment="Center" Height="30" Width="115" mah:TextBoxHelper.ClearTextButton="True" mah:TextBoxHelper.UseFloatingWatermark="True" mah:TextBoxHelper.Watermark="Search..."> <TextBox.InputBindings> <KeyBinding Key="Return" Command="{Binding TextBoxButtonCmd, Mode=OneWay}" CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TextBox}, Path=Text, Mode=OneWay}" /> </TextBox.InputBindings> <TextBox.Style> <Style BasedOn="{StaticResource MahApps.Styles.TextBox.Search}" TargetType="{x:Type TextBox}"> <Style.Triggers> <Trigger Property="mah:TextBoxHelper.HasText" Value="True"> <Setter Property="mah:TextBoxHelper.ButtonContent" Value="r" /> <Setter Property="mah:TextBoxHelper.ButtonContentTemplate" Value="{x:Null}" /> </Trigger> </Style.Triggers> </Style> </TextBox.Style> </TextBox> </StackPanel> <!--系统托盘--> <tb:TaskbarIcon x:Name="myNotifyIcon" Visibility="Visible" ToolTipText="" IconSource="Logo.ico" DoubleClickCommand="{Binding ShowMainWindowCommand}"> <tb:TaskbarIcon.ContextMenu> <!--添加菜单--> <ContextMenu> <MenuItem Header="显示" ToolTip="显示主界面" Command="{Binding ShowMainWindowCommand}" /> <MenuItem Header="退出" ToolTip="退出程序" Command="{Binding ExitMainWindowCommand}" /> </ContextMenu> </tb:TaskbarIcon.ContextMenu> </tb:TaskbarIcon> </Grid> </mah:MetroWindow> ``` 整体分为: * 主界面:提供各功能入口; * 左侧边栏和中间采用tabcontrol实现 * 底部按钮栏:提供全局配置等功能入口; * 系统托盘:提供界面显示、隐藏和软件退出等功能; ### 2.模型定义 目前只定义了和快捷访问相关模型: ```c# class shortcut : BindableBase { private string _head; public string Head { get { return _head; } set { SetProperty(ref _head, value); } } private List<shortcutinfo> _fileInfo; public List<shortcutinfo> FileInfo { get { return _fileInfo; } set { SetProperty(ref _fileInfo, value); } } public shortcut(string head, List<shortcutinfo> file) { Head = head; FileInfo = file; } public shortcut() { } private DelegateCommand<object> _clickRunCommnd; public DelegateCommand<object> ClickRunCommnd => _clickRunCommnd ?? (_clickRunCommnd = new DelegateCommand<object>(ExecuteClickCommnd)); void ExecuteClickCommnd(object obj) { var e = obj as DragEventArgs; if (e!=null && e.Data.GetDataPresent(DataFormats.FileDrop)) { // Note that you can have more than one file. string[] files = (string[])e.Data.GetData(DataFormats.FileDrop); foreach(var file in files) { //判断是否为快捷方式 string targetFile = file; if(file.ToLower().EndsWith(".lnk")) { targetFile = FileIcon.toLinkAimFilePath(file); } //获取文件ico var ico = FileIcon.getIcon(targetFile); shortcutinfo info = new shortcutinfo(); info.Ico = FileIcon.ToImageSource(ico); //判断是否为 .exe文件 info.FileName = Path.GetFileName(targetFile); if(info.FileName.ToLower().EndsWith(".exe")) { info.FileName = info.FileName.ToLower().Replace(".exe", ""); } if(info.FileName == "") { info.FileName = Path.GetFileNameWithoutExtension(targetFile); } info.FullfilePath = targetFile; FileInfo.Add(info); FileInfo = new List<shortcutinfo>(FileInfo); } } } } class shortcutinfo : BindableBase { private string fileName; public string FileName { get { return fileName; } set { SetProperty(ref fileName, value); } } private ImageSource ico; public ImageSource Ico { get { return ico; } set { SetProperty(ref ico, value); } } private string fullfilePath; public string FullfilePath { get { return fullfilePath; } set { SetProperty(ref fullfilePath, value); } } public shortcutinfo(string _fileName, ImageSource _ico, string _fullfilePath) { FileName = _fileName; Ico = _ico; FullfilePath = _fullfilePath; } public shortcutinfo() { } } ``` 以及直接和UI层打交道的:MainViewModel ```c# class MainViewModel : BindableBase { IEventAggregator _ea; private string _softTitle; public string SoftTitle { get { return _softTitle; } set { SetProperty(ref _softTitle, value); } } private List<shortcut> _fileLists; public List<shortcut> FileLists { get { return _fileLists; } set { SetProperty(ref _fileLists, value); } } private Visibility _mainWindowVisiable; public Visibility MainWindowVisiable { get { return _mainWindowVisiable; } set { SetProperty(ref _mainWindowVisiable, value); } } public MainViewModel(IEventAggregator ea) { MainWindowVisiable = Visibility.Visible; FileLists = new List<shortcut>(); SoftTitle = "SoftMa"; List<Box> list; try { list = XmlHelper.XmlDeserializeFromFile<List<Box>>("SoftMa.Box.xml", Encoding.UTF8); foreach (var li in list) { shortcut shortcut_ = new shortcut(); shortcut_.Head = li.CommandName; shortcut_.FileInfo = new List<shortcutinfo>(); foreach (var fi in li.Parameters) { shortcutinfo info = new shortcutinfo(); info.FileName = fi.Name; info.FullfilePath = fi.FullName; info.Ico = FileIcon.ToImageSource(FileIcon.getIcon(info.FullfilePath)); shortcut_.FileInfo.Add(info); } FileLists.Add(shortcut_); } } catch (Exception ex) { MessageBox.Show(ex.Message); } _ea = ea; _ea.GetEvent<AddBoxEvent>().Subscribe(AddBoxMessageReceived);//订阅事件 } private void SaveConfig() { List<Box> Boxs = new List<Box>(); foreach (var lists in FileLists) { Box b = new Box(); b.CommandName = lists.Head; b.Parameters = new List<Parameter>(); foreach(var info in lists.FileInfo) { Parameter p = new Parameter(); p.Name = info.FileName; p.FullName = info.FullfilePath; b.Parameters.Add(p); } Boxs.Add(b); } try { XmlHelper.XmlSerializeToFile(Boxs, "SoftMa.Box.xml", Encoding.UTF8); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private DelegateCommand _showMainWindowCommand; public DelegateCommand ShowMainWindowCommand => _showMainWindowCommand ?? (_showMainWindowCommand = new DelegateCommand(()=>{ MainWindowVisiable = Visibility.Visible; })); private DelegateCommand _hideMainWindowCommand; public DelegateCommand HideMainWindowCommand => _hideMainWindowCommand ?? (_hideMainWindowCommand = new DelegateCommand(() => { MainWindowVisiable = Visibility.Hidden; })); private DelegateCommand _exitMainWindowCommand; public DelegateCommand ExitMainWindowCommand => _exitMainWindowCommand ?? (_exitMainWindowCommand = new DelegateCommand(() => { Application.Current.Shutdown(); SaveConfig(); })); private void AddBoxMessageReceived(shortcut cut) { if(FileLists.Contains(cut)) { FileLists.Remove(cut); } } } ``` ### 3.APP与配置文件(.xml)的交互 使用网上代码进行xml的序列化和反序列化: > 此处代码来源于博客【在.net中读写config文件的各种方法】的示例代码 > http://www.cnblogs.com/fish-li/archive/2011/12/18/2292037.html ```c# public static class XmlHelper { private static void XmlSerializeInternal(Stream stream, object o, Encoding encoding) { if (o == null) throw new ArgumentNullException("o"); if (encoding == null) throw new ArgumentNullException("encoding"); XmlSerializer serializer = new XmlSerializer(o.GetType()); XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.NewLineChars = "\r\n"; settings.Encoding = encoding; settings.IndentChars = " "; using (XmlWriter writer = XmlWriter.Create(stream, settings)) { serializer.Serialize(writer, o); writer.Close(); } } /// <summary> /// 将一个对象序列化为XML字符串 /// </summary> /// <param name="o">要序列化的对象</param> /// <param name="encoding">编码方式</param> /// <returns>序列化产生的XML字符串</returns> public static string XmlSerialize(object o, Encoding encoding) { using (MemoryStream stream = new MemoryStream()) { XmlSerializeInternal(stream, o, encoding); stream.Position = 0; using (StreamReader reader = new StreamReader(stream, encoding)) { return reader.ReadToEnd(); } } } /// <summary> /// 将一个对象按XML序列化的方式写入到一个文件 /// </summary> /// <param name="o">要序列化的对象</param> /// <param name="path">保存文件路径</param> /// <param name="encoding">编码方式</param> public static void XmlSerializeToFile(object o, string path, Encoding encoding) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException("path"); using (FileStream file = new FileStream(path, FileMode.Create, FileAccess.Write)) { XmlSerializeInternal(file, o, encoding); } } /// <summary> /// 从XML字符串中反序列化对象 /// </summary> /// <typeparam name="T">结果对象类型</typeparam> /// <param name="s">包含对象的XML字符串</param> /// <param name="encoding">编码方式</param> /// <returns>反序列化得到的对象</returns> public static T XmlDeserialize<T>(string s, Encoding encoding) { if (string.IsNullOrEmpty(s)) throw new ArgumentNullException("s"); if (encoding == null) throw new ArgumentNullException("encoding"); XmlSerializer mySerializer = new XmlSerializer(typeof(T)); using (MemoryStream ms = new MemoryStream(encoding.GetBytes(s))) { using (StreamReader sr = new StreamReader(ms, encoding)) { return (T)mySerializer.Deserialize(sr); } } } /// <summary> /// 读入一个文件,并按XML的方式反序列化对象。 /// </summary> /// <typeparam name="T">结果对象类型</typeparam> /// <param name="path">文件路径</param> /// <param name="encoding">编码方式</param> /// <returns>反序列化得到的对象</returns> public static T XmlDeserializeFromFile<T>(string path, Encoding encoding) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException("path"); if (encoding == null) throw new ArgumentNullException("encoding"); string xml = File.ReadAllText(path, encoding); return XmlDeserialize<T>(xml, encoding); } } ``` 通过xmlhelper进行Box的序列号和反序列化: ```C# public class Box { [XmlAttribute("Name")] public string CommandName; [XmlArrayItem("Parameter")] public List<Parameter> Parameters = new List<Parameter>(); } public class Parameter { [XmlAttribute("FileName")] public string Name; [XmlAttribute("FullfilePath")] public string FullName; } ``` ### 4.ico图标文件的提取 ```C# private const uint SHGFI_ICON = 0x100; private const uint SHGFI_LARGEICON = 0x0; //大图标 private const uint SHGFI_SMALLICON = 0x1; //小图标 [StructLayout(LayoutKind.Sequential)] public struct SHFILEINFO { public IntPtr hIcon; //文件的图标句柄 public IntPtr iIcon; //图标的系统索引号 public uint dwAttributes; //文件的属性值 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szDisplayName;//文件的显示名 [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] public string szTypeName; //文件的类型名 }; [DllImport("shell32.dll")] private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags); /// <summary> /// 获取文件FilePath对应的Icon /// </summary> public static Icon getIcon(string FilePath) { SHFILEINFO shinfo = new SHFILEINFO(); //FileInfo info = new FileInfo(FileName); //小图标 SHGetFileInfo(FilePath, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SMALLICON); Icon largeIcon = Icon.FromHandle(shinfo.hIcon); //Icon.ExtractAssociatedIcon(FileName); return largeIcon; } ``` ### 5.获取快捷方式所指向的目标 ```C# public static string toLinkAimFilePath(string linkPath) { if (System.IO.File.Exists(linkPath)) { WshShell shell = new WshShell(); IWshShortcut cut = (IWshShortcut)shell.CreateShortcut(linkPath); return cut.TargetPath; } return ""; } ``` ## 五、总结及下一步计划 ### 1.总结 到目前为止,其实自己写的代码很少,基本上是个缝合怪。 前期对于listview的MouseDoubleClick事件转prism的command纠结太多,无法很好的将当前双击的item作为参数传递给model层。 最后还是妥协了,使用的EventTrigger。 ### 2.下一步计划 进行快捷访问功能晚上,实现完整功能。 ### 3.吐个槽 目前国内wpf相关的资料还是太少了。搜索一个主题,不管是baidu还是bing,结果基本上都是同一篇文章在不同网站的复制粘贴。 同时,很多教程或者资料都是浅尝辄止,对于复杂应用场景,依然还是得靠自己摸索,依旧是有众多巨大的坑需要去踩。 prism官网的视频教程,居然是收费的!!!- ## 六、截图  最后修改:2022 年 04 月 23 日 11 : 46 PM © 禁止转载
1 条评论
(๑•̀ㅁ•́ฅ)