前言
发现实习了以后自己就变懒了,每天回到宿舍只想打游戏,看剧。要是能有人家考研的一半劲头就好了。
写完Everything的C#实现之后呢,我想写一些工作期间遇到的问题。一来当作笔记,二来加深学习。
UI
磁盘图标
首先呢,我们先做一个可以显示各磁盘空间的控件。这里其实是一个个使用了GDI绘图后的Panel,我采用FlowLayoutPanel作为它的容器。容器的宽度作为参数之一,用它来决定Panel的大小,绘图的尺寸。绘制也很简单,先画一个绿色的大圆,根据比例,在其之上再画一个红色的扇形,然后再覆盖一个白色的小圆,三者同心,这样就是一个可以展示磁盘空间占用比例的圆环了。最后在中间写上文字即可,很简单吧!
我还用GDI+Winform做过很多小游戏,例如贪吃蛇,五子棋,你画我猜等等,觉得很好玩。后来接触到WPF,一时间接受不了。它的动画效果怎么这么棒!写起来怎么怎么方便!再后来,见识到了眼花缭乱的前端技术,我便不再对UI设计抱有什么梦想了,还是后端比较单纯。
过滤器图标
再说过滤器控件,Everything中的过滤器是一条菜单项,我将其可视化为了UI图形。它由一个PictureBox,一个CheckBox,以及两个Label组成。同样采用FlowLayoutPanel作为它的容器。与上面那个磁盘圆环一样,我将它们上面的事件,属性封装成了一个UI库文件。
过滤器控件相对来说复杂一些,为了可以让用户自由选择图标,并且控件本身要记录代表的文件类型,它的三个组成部分还要表现的像一个整体一样。封装时,我给这个控件添加了四个public属性,分别为Name,FileNumber,IsCheck,以及图片的path, 并且完善了点击事件联动代码,图片显示,出错处理等代码。
查询
让我们先来明确一下查询的条件是什么。
- 需要支持按照文件名称模糊查询。
- 需要支持按照文件类型进行查询,系统肯定不能定义出所有的文件类别,因此还需要让用户可以自定义查询类型。
- 需要支持正则表达式查询。
- 需要支持特定的目录下查询。
文件名模糊查询很简单,我这里使用Contains函数来完成。文件类型查询怎么做到呢?我们通过观察Everything可以发现它是按照文件扩展名来做的。那我们就用一个List集合来保存多个扩展名,并用它代表某一类文件,做成文件过滤器吧!为了能够让用户的配置可以保存下来,我使用到了XML。并且在程序中定义了一个与其对应的Model,叫做Filter。
<?xml version="1.0" encoding="UTF-8"?>
<filters>
<filter>
<name>实验文档</name>
<url>E:\图片\壁纸\《ARC X Windows 10 Theme》简约黑白风景壁纸_彼岸图网.jpg</url>
<param>.DOC</param>
<param>.DOCX</param>
</filter>
</filters>
public class Filter
{
public Filter(string name, List<string> param, string url)
{
this.name = name;
this.param = new List<string>();
foreach (string p in param)
{
if (p.StartsWith("."))
{
this.param.Add(p.ToUpper());
}
else
{
this.param.Add(string.Concat(".", p).ToUpper());
}
}
this.url = url;
}
private string name;
public string Name { get=>name; set=>name=value; }
private List<string> param;
public List<string> Param {
get =>param;
set
{
foreach (string p in value)
{
if (p.StartsWith("."))
{
this.param.Add(p.ToUpper());
}
else
{
this.param.Add(string.Concat(".", p).ToUpper());
}
}
}
}
private string url;
public string Url { get=>url; set=>url=value; }
}
上面介绍到的磁盘UI控件的构造函数接受传入一个DriveInfo对象,我封装好的GDI方法会完成绘图。
过滤器控件则不同。我会将用户自定义的过滤器从xml中解析出来,保存至List中,过滤器控件依据Filter的个数而创建。其中,过滤器的Name就取自该Filter在List中的Index。这样,当我们进行查询时,可以遍历每一个过滤器控件,将被选中的(IsCheck=true)过滤器控件的名字转换成Index,找到List中对应的Filter,取出它们的List Param,拼接成一个List,作为查询条件之一。
这样便实现了多种类组合查询的功能。为了能够让用户对过滤器进行必要的增删改查操作,我们需要书写一个包含各种方法的xml工具类。当然使用xpath就很简单。
判断文件是否存在,不存在则创建一个
存在则创建XmlDocument对象。取出根节点
root = xmlfilter.SelectSingleNode("filters");
查
public List<Filter> ReadXml() { List<Filter> filters = new List<Filter>(); foreach (XmlNode item in root) { XmlElement xm = (XmlElement)item; string name = xm.SelectSingleNode("name").InnerText; string url = xm.SelectSingleNode("url").InnerText; List<string> param = new List<string>(); foreach (XmlNode c in xm.SelectNodes("param")) { param.Add(c.InnerText.ToString().ToLower()); } Filter filter = new Filter(name, param, url); filters.Add(filter); } return filters; }
添加子节点
private void CreateNode(XmlDocument xmlDoc, XmlNode parentNode, string name, string value) { XmlNode node = xmlDoc.CreateNode(XmlNodeType.Element, name, null); node.InnerText = value; parentNode.AppendChild(node); }
其他的功能就不再介绍了。
再来看剩下的两个查询需求,正则查询和指定文件目录查询。这两个功能就很简单了。指定目录也就是文件路径以什么字符串开头 ;正则查询又不需要我们自己写正则,在界面上添加个CheckBox,让用户自己选择是正则查询,还是普通查询就好了,使用C#中的Regex类即可完成。
因此,虽然我们的提供查询功能很丰富,实际上只需要写一个重载的查询函数即可——正则查询和普通查询。
/// <summary>
/// 查询文件
/// </summary>
/// <param name="startWith"></param>
/// <param name="inputs"></param>
/// <param name="filters"></param>
/// <returns></returns>
public IEnumerable<FileStruct> SearchMatchFileMethod(string startWith,string inputs, List<string> filters)
{
if (string.IsNullOrEmpty(inputs) && string.IsNullOrEmpty(startWith))
return _fileSource.Files;
if (filters.Contains(".*"))
return _fileSource.Files.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount).
Where<FileStruct>(r => r.Path.StartsWith(startWith) && r.Name.Contains(inputs)) ;
return _fileSource.Files.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount).
Where<FileStruct>(r => r.Path.StartsWith(startWith) && r.Name.Contains(inputs) && filters.Contains(r.ExpandedName));
}
/// <summary>
/// 根据正则表达式查询文件
/// </summary>
/// <param name="startWith"></param>
/// <param name="regex"></param>
/// <param name="filters"></param>
/// <returns></returns>
public IEnumerable<FileStruct> SearchMatchFileMethod(string startWith, Regex regex, List<string> filters)
{
if (filters.Contains(".*"))
return _fileSource.Files.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount).
Where<FileStruct>(r => r.Path.StartsWith(startWith) && regex.IsMatch(r.Name));
return _fileSource.Files.AsParallel().WithDegreeOfParallelism(Environment.ProcessorCount).
Where<FileStruct>(r => r.Path.StartsWith(startWith) && regex.IsMatch(r.Name) && filters.Contains(r.ExpandedName));
}
数据可视化
为了使用HighChart画饼图,Winform原生web控件不好用。安装CefSharp包,使用ChromiumWebBrowser。
ChromiumWebBrowser CWebBrowser;
private void AnalysisView_Load(object sender, EventArgs e)
{
CWebBrowser = new ChromiumWebBrowser(Environment.CurrentDirectory + "\\view.html");//
this.panel1.Controls.Add(CWebBrowser);
CWebBrowser.Dock = DockStyle.Fill;
CWebBrowser.ContextMenu = new ContextMenu();
CWebBrowser.FrameLoadEnd += new EventHandler<FrameLoadEndEventArgs>(FrameLoadEnd_Event);
}
执行js脚本也超级简单!
CWebBrowser.EvaluateScriptAsync(script)即可!!!
private void FrameLoadEnd_Event(object sender,FrameLoadEndEventArgs e)
{
List<string> param = new List<string>();
param.Add(".*");
for (int i = 0; i < Program.fileSource.Drives.Length; i++)
{
DriveInfo d = Program.fileSource.Drives[i];
int count = Program.fileParallelQuery.SearchMatchFileMethod(d.Name, "", param).Count();
string script = "drivechange(" + i.ToString() + ",'" + d.Name.Trim('\\') + "'," + count.ToString() + ")";
CWebBrowser.EvaluateScriptAsync(script);
}
for (int i = 0; i < Program.fileSource.ClassificNumber.Count; i++)
{
string name = i<Program.filterSource.SystemFilters.Count-1?
Program.filterSource.SystemFilters[i+1].Name//i+1是为了不算上 "全部"
: Program.filterSource.UserFilters[i+1- Program.filterSource.SystemFilters.Count].Name;
int count;
Program.fileSource.ClassificNumber.TryGetValue(i, out count);
string script = "filterchange(" + (i - 1).ToString() + ",'" + name + "'," + count.ToString() + ")";
CWebBrowser.EvaluateScriptAsync(script);
}
string recordPath = Environment.CurrentDirectory + "\\countrecord.txt";
using (StreamReader sr = new StreamReader(recordPath))
{
string records = sr.ReadToEnd();
string[] clauses = records.Split('\n');
if (clauses.Length > 12)
{
clauses = clauses.Skip<string>(clauses.Length - 12).ToArray<string>();
}
for (int i = 0; i < clauses.Length - 1; i++)
{
string[] info = clauses[i].Split('|');
string script = "countchange(" + i + ",'" + info[0] + "'," + info[1] + ")";
CWebBrowser.EvaluateScriptAsync(script);
}
}
}
Shell编程之鼠标右键
想获得Windows资源管理器的鼠标右键功能,需要先了解Windows外壳名字空间以及Windows shell编程。去看这位大神的博客吧!
其中最关的一点就是要获取到:所选中文件的父节点的 IShellFolder 接口和该文件的pidl。 ShellApi.GetPShellAndPIDL是我封装好的一个方法。
IntPtr[] pidls = new IntPtr[1];
IShellFolder IParent;
//获得父节点的 IShellFolder 接口 和该文件的pidl
ShellApi.GetPShellAndPIDL(iDeskTop, out IParent, out pidls[0], file);
//得到 IContextMenu 接口
IntPtr iContextMenuPtr = IntPtr.Zero;
iContextMenuPtr = IParent.GetUIObjectOf(IntPtr.Zero, (uint)pidls.Length,
pidls, ref Guids.IID_IContextMenu, out iContextMenuPtr);
IContextMenu iContextMenu = (IContextMenu)Marshal.GetObjectForIUnknown(iContextMenuPtr);
//提供一个弹出式菜单的句柄
IntPtr contextMenu = API.CreatePopupMenu();
iContextMenu.QueryContextMenu(contextMenu, 0,
API.CMD_FIRST, API.CMD_LAST, CMF.NORMAL | CMF.EXPLORE);
//弹出菜单
uint cmd = API.TrackPopupMenuEx(contextMenu, TPM.RETURNCMD,
MousePosition.X, MousePosition.Y, this.Handle, IntPtr.Zero);
//获取命令序号,执行菜单命令
if (cmd >= API.CMD_FIRST)
{
CMINVOKECOMMANDINFOEX invoke = new CMINVOKECOMMANDINFOEX();
invoke.cbSize = Marshal.SizeOf(typeof(CMINVOKECOMMANDINFOEX));
invoke.lpVerb = (IntPtr)(cmd - 1);
invoke.lpDirectory = string.Empty;
invoke.fMask = 0;
invoke.ptInvoke = new POINT(MousePosition.X, MousePosition.Y);
invoke.nShow = 1;
iContextMenu.InvokeCommand(ref invoke);
}
结语
这个程序做起来挺累的。没人指导,啥都得自己摸索。挺简单的一个东西可能要捣鼓好几天。做完以后就觉得好简单,没学到什么东西一样,写博客的时候总觉得脑袋里没东西。其实里边任意一个模块都值得深入去了解一下的,而我比较懒,交完作业就放那了。
而且从恣意妄为的代码风格应该可以看出,我是个野生程序员。
源码