工作是枯燥的,重复性的工作,更是枯燥中的枯燥;不幸的是,最近有个非常枯燥而又繁重的任务,让我老眼昏花心跳加速,感叹时光补与,其实任务很简单,就是统计客户订单后,需要把订单的属性保存到excel上传存档……
在一遍又一遍地打开excel以后,我发现这些订单有一个特点:订单号是递增的,而属性,绝大部分是重复的,于是我的偷懒天赋躁动不安起来,我不禁思考:能不能让电脑自动化操作,帮我分担一些任务呢?
说到自动化脚本,最早我是想用python的,但pyQt操作excel的库不太方便,我甚至异想天开地想用win API……
不过,等等!
既然说到win,为什么不用微软全家桶呢?
通过百度大法,我知道了C#+VS+winform这样糖分巨高的组合,而对于excel的脚本,微软贴心地准备好了各种api接口给你调用,这……比python的excel库不知高到哪里去了。(虽然用起来稍微麻烦一点)
接下来花了一天时间简单浏览了一下c#的官方教程,然后又扫了一眼excel的库,不得不说,MSDN太贴心了。
好了,预热结束,准备开搞,不过,在开搞之前,我得先准备点东西:
1,下载并安装VS2019 community,全程next无压力,就是……安装包有点大。好在我的百兆水管还能凑活;
2,将浏览器搜索工具由百度切换为bing,既然是面向搜索引擎编程,那就得换个好的工具不是(主要是bing对msdn特别友好,官方文档一搜就有)
做完这些准备工作之后,那就开始编(ctrl + C)程(ctrl + V)吧!
工具由三个主要部分组成:
- 录入界面
- excel操作
- 截图保存
1,录入界面
这部分主要是考虑工具的可视化,用界面把重复信息录入,方便后续批量操作。
至于界面,当然用winform了,如果不需要用界面,跳过此步骤即可。
1.1创建窗体应用程序
1.2拖控件
1.3写代码
直接在控件上双击,就能写相应的代码了,比如,我这个需要录入单号、厂家、客户信息,然后自动赋值给我的excel操作类,那么可以直接 在文本框双击,写下以下代码:
private
我需要实现点击Generate即自动生成报告,那么双击Generate控件,直接写代码:
private void button_Generate_Click(object sender, EventArgs e)
{
Task task = new Task(SaveFile);
task.Start();
}
具体的操作后面再细讲。
2,excel操作
微软针对office互操作提供了API,官方文档如下,我只需要操作excel即可。
https://docs.microsoft.com/en-us/dotnet/api/microsoft.office.interop.excel?view=excel-piadocs.microsoft.com2.1 interop.excel介绍
excel的API操作逻辑是这样的:excel应用->workbook->worksheet->cell
第一步:创建一个excel应用:
excelApp = new Microsoft.Office.Interop.Excel.Application();
excelApp.Visible = false;//这里将excel设为不可见,如果你想看到excel自动操作的界面,那么可以设为true
第二步:创建workbook和worksheet
Workbook wkb = excelApp.Workbooks.Add();//新建
//Workbook wkb = excelApp.Workbooks.Open(filename);//打开一个现有文件,文件名为filename;
Worksheet wks = wkb.Worksheets["Sheet1"];
需要注意的是,如果本机没有安装office,是不能打开现有excel文件的,只能创建(为啥提这个,因为我电脑上只装了WPS)。
第三步:操作单元格:
Microsoft.Office.Interop.Excel.Range rng = wks.get_Range("B1");//选取单元格
//Microsoft.Office.Interop.Excel.Range rng = wks.get_Range("B1:C14");//选取一组单元格,范围从B1至C14
rng.Value = "test";//填充单元格的值
2.2 插入OLE对象
除了将界面上的文字信息填入excel之外,我还希望将本地文件作为对象插入到excel,要怎么操作呢?
interop.excel提供了OLEObjects供提供。
OLEObjects
2.4 插入图片
interop.excel不提供直接将图像对象插入excel的方法,那么有两种办法插入图片,
一是将图像复制到剪切板,再从剪切板插入;
//Clipboard.SetImage(this.ImgOne);//直接操作剪切板是没用的,会报错,需要将这个方法用委托来实现。
public delegate void setClipBoard(Image img);//这个委托需要放到函数外;
setClipBoard setClip = new setClipBoard(setCB);//这是自定义的将图像设置到系统剪切板的函数
setClip.Invoke(this.ImgOne);//将截图复制到剪切板
int shapeCount;
rng = wks.get_Range("B30");
wks.Paste(rng);
shapeCount = wks.Shapes.Count;//图像插入以后,就是一个shape,记录下shape的数量,方便操作。
Console.WriteLine("shapeCount=" + shapeCount);
Shape tempShape = wks.Shapes.Item(shapeCount);
//设置shape也就是刚刚插入的图片的缩放
if(tempShape.Height > 100)
{
Console.WriteLine("resize pic");
//按高度缩放
tempShape.ScaleHeight(100/tempShape.Height,Microsoft.Office.Core.MsoTriState.msoCTrue);
}
setCB定义如下
public void setCB(Image img) { Clipboard.SetImage(img); }
二是从文件插入:
rng = wks.get_Range("B30");
pic = range.Worksheet.Shapes.AddPicture(filePath, Microsoft.Office.Core.MsoTriState.msoFalse, Microsoft.Office.Core.MsoTriState.msoCTrue, rng.Left, rng.Top, 300, 300)
我采用的是第一种,因为我需要截图,如果把截图保存到文件再插入,那就太麻烦了。
到这里,自动填充excel基本功能都实现了。
下面的截图保存是个额外功能,和excel没关系,可以不看。
3,截图保存
3.1屏幕截图的基本原理
原理呢,就是新建一个Form,放到最前面,然后把当前屏幕的图像设为Form的背景图,其实是有点取巧的意见。
3.2截图操作
新建一个空白 Form,注意要将事件绑定为自己写的截图函数
双击窗体,写下初始化代码设置
private void CutPic_Load(object sender, EventArgs e)
{
this.KeyPreview = true;//保持窗体先接收键盘消息,用来绑定ESC退出事件
this.Image = this.BackgroundImage;//将屏幕图像设置为Form背景
CatchFinished = false;//用来判断是否完成截图
CatchStart = false;//用来判断是否开始截图
isDowned = false;//用来判断是否鼠标按下
}
如果按下ESC,退出截图
private void CutPic_KeyDown(object sender,KeyEventArgs e)
{
if(e.KeyCode == Keys.Escape)
{
this.Close();
}
}
如果按下右键,也退出; 如果是左键,则准备画画;
private void CutPic_MouseDown(object sender, MouseEventArgs e)
{
//左键准备画,设置判断属性
if(e.Button == MouseButtons.Left)
{
if (!CatchStart)
{
CatchStart = true;
// 保存此时鼠标按下坐标
DownPoint = new Point(e.X, e.Y);
}
}
//右键退出
if (MouseButtons.Right == e.Button)
{
this.DialogResult = DialogResult.OK;
this.Close();
}
如果移动鼠标,同时左键按下,则画矩形框:
如果没有按下,只是移动鼠标,则根据鼠标位置画十字线(比较笨的实现)
private void CutPic_MouseMove(object sender, MouseEventArgs e)
{
//如果按下了,则开始画
if (CatchStart)
{
//新建一个图像
Bitmap copyMap = new Bitmap(Screen.AllScreens[0].Bounds.Width, Screen.AllScreens[0].Bounds.Height);
//新建起始点
Point newPoint = new Point(DownPoint.X, DownPoint.Y);
//将图像复制一下
copyMap = (Bitmap)this.BackgroundImage.Clone();
//新建一个画面
Graphics g = Graphics.FromImage(copyMap);
//新建画笔
Pen pen = new Pen(Color.Red, 1);
int width = Math.Abs(e.X - DownPoint.X);
int height = Math.Abs(e.Y - DownPoint.Y);
//如果是反向移动鼠标,则把矩形的起点调换下
if(e.X < DownPoint.X)
{
newPoint.X = e.X;
}
if(e.Y < DownPoint.Y)
{
newPoint.Y = e.Y;
}
//画矩形
CatchRectangle = new Rectangle(newPoint, new Size(width, height));
g.DrawRectangle(pen, CatchRectangle);
//抛弃对象,省点内存
g.Dispose();
pen.Dispose();
//画完之后,重新新建画布,再把画面刷新一下,否则,你会看到屏幕上画满了矩形,
Graphics g1 = this.CreateGraphics();
g1.DrawImage(copyMap, new Point(0, 0));
g1.Dispose();
copyMap.Dispose();
}
//如果鼠标没按下,则根据鼠标位置画十字线:
else
{
//先刷新一下画面,否则上一次画的十字线会停留在屏幕上,这是个很笨的办法就是了。
this.Refresh();
//新建画布
Graphics g = this.CreateGraphics();
//新建画笔
Pen pen = new Pen(Color.Green, 1);
//画线
//draw vertical line
g.DrawLine(pen, new Point(e.X, 0), new Point(e.X, this.Height));
//draw horizontal line
g.DrawLine(pen, new Point(0, e.Y), new Point(this.Width, e.Y));
//抛弃对象,省点内存
g.Dispose();
pen.Dispose();
}
}
画图完成,鼠标按起,则把矩形里的图像截图:
private void CutPic_MouseUp(object sender, MouseEventArgs e)
{
if(CatchRectangle.Width == 0 | CatchRectangle.Height == 0)
{
CatchStart = false;
return;
}
else if(e.Button == MouseButtons.Left)
{
if(CatchStart == true)
{
CatchStart = false;
CatchFinished = true;
//将图像复制一下
Bitmap tempMap = (Bitmap)this.BackgroundImage.Clone();
toDrawMap = new Bitmap(CatchRectangle.Width, CatchRectangle.Height);
//新建画布
Graphics g = Graphics.FromImage(toDrawMap);
//画图
g.DrawImage(tempMap, new Rectangle(0,0,CatchRectangle.Width,CatchRectangle.Height), CatchRectangle, GraphicsUnit.Pixel);
//将截图保存到剪切板,这里可以直接操作,如果是在非GUI线程,是不能直接操作ClipBoard的,比如刚刚的excelApp
Clipboard.SetImage(toDrawMap);
sendMapToFrom1(toDrawMap);//事件,通知主程序,我画完了,同时把截图传过去,显示在主程序界面
g.Dispose();
tempMap.Dispose();
tempMap.Dispose();
this.Close();//画图完成,退出
}
}
}
对于工具的截图操作,有个不方便的地方,点一下截图,会把当前的程序界面也截图了,那么采用的办法有一个,截图开始前,将主程序最小化并隐藏:
this.WindowState = FormWindowState.Minimized;
this.Hide();
Thread.sleep(100);//为啥要加一个100ms等待,因为这个程序在另外一台win7上跑的时候发现最小化的时间有点长,还是被捕捉到截图里了。只想到这个笨办法来避免。
这里重点提供一下事件绑定,在主界面里新建截图界面的时候,要把这个对象的事件消息绑定一下,如下:
CutPic cutter = new CutPic();
cutter.WindowState = FormWindowState.Maximized;
cutter.BackgroundImage = img;
cutter.BackgroundImageLayout = ImageLayout.Zoom;
cutter.sendMapToFrom1 += new CutPic.SendMap(sendmaptwo);//绑定事件消息
cutter.Show();
那么这个事件消息在哪里设置的呢?在截图界面里定义
public partial class CutPic : Form
{
//设置委托
public delegate void SendMap(Bitmap bm);
//定义事件
public event SendMap sendMapToFrom;
private Point DownPoint;
protected bool m_bMoving;
protected bool m_bChangedWidth;
protected bool m_bChangeHeight;
protected bool m_bMouseHover;
private bool _IsDrawed;
private bool isDowned;
private Image _Image;
private bool CatchStart;
private Rectangle CatchRectangle;
private bool CatchFinished;
private Bitmap fullmaskMap;
private Rectangle maskRectangle;
private Pen maskPen;
private SolidBrush maskBrush;
Bitmap toDrawMap;
}
主界面针对收到的消息,进行处理,如下:
public void sendmaptwo(Bitmap bm)
{
if (bm != null)
{
//将excelApp中的图像设置为刚刚的截图
newReport.ImgTwo = bm;
//将截图同步显示在主界面图片框里
this.pictureBox2.Image = bm;
this.pictureBox2.SizeMode = PictureBoxSizeMode.Zoom;
}
//已知截图完成,将主程序最大化并显示
this.Show();
this.WindowState = FormWindowState.Normal;
isDrawing = false;
}
4,热键绑定
即使有了程序,当然也想用热键来提升效率,怎么办呢。
以下是从网上抄来的办法,具体的程序定义还没弄太明白,将就着用
//定义了辅助键的名称(将数字转变为字符以便于记忆,也可去除此枚举而直接使用数值)
[Flags()]
public enum KeyModifiers
{
None = 0,
Alt = 1,
Ctrl = 2,
Shift = 4,
WindowsKey = 8
}
//引入user32
[DllImport("user32.dll")]
public static extern bool RegisterHotKey(
IntPtr hWnd, //要定义热键的窗口的句柄
int id, //定义热键ID(不能与其它ID重复)
KeyModifiers fsModifiers, //标识热键是否在按Alt、Ctrl、Shift、Windows等键时才会生效
Keys vk //定义热键的内容
);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool UnregisterHotKey(
IntPtr hWnd, //要取消热键的窗口的句柄
int id //要取消热键的ID
);
/// <summary>
/// 注册热键
/// </summary>
/// <param name="hwnd">窗口句柄</param>
/// <param name="hotKey_id">热键ID</param>
/// <param name="keyModifiers">组合键</param>
/// <param name="key">热键</param>
public static void RegKey(IntPtr hwnd, int hotKey_id, KeyModifiers keyModifiers, Keys key)
{
try
{
if (!RegisterHotKey(hwnd, hotKey_id, keyModifiers, key))
{
if (Marshal.GetLastWin32Error() == 1409) { MessageBox.Show("热键被占用 !"); }
else
{
MessageBox.Show("注册热键失败!");
}
}
}
catch (Exception e) {
MessageBox.Show("注册热键失败:" + e.ToString());
}
}
/// <summary>
/// 注销热键
/// </summary>
/// <param name="hwnd">窗口句柄</param>
/// <param name="hotKey_id">热键ID</param>
public static void UnRegKey(IntPtr hwnd, int hotKey_id)
{
//注销Id号为hotKey_id的热键设定
UnregisterHotKey(hwnd, hotKey_id);
}
绑定热键
private bool isDrawing;
private const int WM_HOTKEY = 0x312; //窗口消息-热键
private const int WM_CREATE = 0x1; //窗口消息-创建
private const int WM_DESTROY = 0x2; //窗口消息-销毁
private const int KeyOneId = 0x34567; //热键ID
private const int KeyTwoId = 0x34568; //热键ID
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
switch (m.Msg)
{
case WM_HOTKEY: //窗口消息-热键ID
switch (m.WParam.ToInt32())
{
case KeyOneId: //热键ID
if(isDrawing == false)
{
this.WindowState = FormWindowState.Minimized;
this.Hide();
ShowCutPic();
}
break;
case KeyTwoId:
if (isDrawing == false)
{
this.WindowState = FormWindowState.Minimized;
this.Hide();
ShowCutPicTwo();
}
break;
default:
break;
}
break;
// D1 35 The 1(one) key.
// D2 36 The 2 key.
// D3 37 The 3 key.
case WM_CREATE: //窗口消息-创建
RegKey(this.Handle, KeyOneId, KeyModifiers.Ctrl, Keys.D1);//Ctrl + 1
RegKey(this.Handle, KeyTwoId, KeyModifiers.Ctrl, Keys.D2);//Ctrl + 2
break;
case WM_DESTROY: //窗口消息-销毁
UnRegKey(this.Handle, KeyOneId); //销毁热键
UnRegKey(this.Handle, KeyTwoId);
break;
default:
break;
}
}
完结
后续要做的,是打算开发小爬虫读取网页上的客户信息,再传到这个APP里自动保存。不过C#里怎么实现,还要研究下。