WPF实现WORD 2013墨迹批注功能
1 前言
WORD 2013可以使用墨迹在文档上面标注,本文讲述通过WPF第三方控件实现类似主要功能如下:
名称 | 描述 |
墨迹标注 | 不论是否触摸屏环境下可以开始墨迹功能,并实现鼠标/触摸在文档任意位置绘制痕迹 |
墨迹痕迹保存 | 绘制的墨迹能够完整在word中保存 |
墨迹参数设置 | 设置墨迹的颜色和线条粗细 |
墨迹擦除 | 提供擦除工具,可以擦除已经绘制的墨迹 |
2 环境及三方组件
名称 | 描述 |
DevExpress V16.2 | 使用其RichEditor控件作为word文档查看和编辑控件 |
Aspose.Word | 在保存word文档时,调整墨迹痕迹的一些属性 |
.NetFrameWork 4.5 | .net运行库环境 |
3 实现思路
首先来看一下word中如何开启墨迹。这个就要说微软不地道了,微软规定了只有在触摸屏的windows环境下才会默认显示墨迹按钮,使用鼠标的就默认不显示,当然我们也可以在选项中强制加上,然后没有啥用,因为使用鼠标的用户会发现“开始墨迹书写”按钮是灰色的,而触屏下是可用的,如下图。
我们先摒弃这个梗不说,为什么要自己开发一个墨迹功能呢,可能是因为office太贵了,一般客户买不起或者不想买,于是就要开发一个包含word基本功能的替代品,也有可能是我们开发软性时候需要集成word类似的功能,或者其他种种原因,所以还是决定自己开发(模仿)一个word的墨迹功能。废话少说,下面来一步一步的分析和设计墨迹功能实现思路。
首先,我找到一个在触摸屏上面写好一些文字的文档,在非触摸屏电脑上打开,选中墨迹,这时候工具栏中“图片”选项卡被激活了,可见所谓的“墨迹”实际上只是将手写痕迹保存为类似图片存储起来了,如下图。
总结一下,墨迹保存为图片的主要属性有这样:
1.允许重叠(好像默认插入图片是不允许重叠的)
2.绝对位置(应该是左上角的坐标位置吧)
3.浮于文字上方(这个肯定的,避免扰乱文字布局)
有了这些属性,我大胆设想如果我使用电子手写板插件,将手写痕迹保存下来,然后赋予这些属性,应该也可以达到墨迹的效果。
现在到了框架选型的阶段,首先我需要一个能够打开和编辑word的UI组件,然后我还需要一个电子手写板插件,最后将他们组合起来。这里我选择了DEV V1.6.2 的WPF版本和WPF的InkCanvas组件,这俩我都在实际中用过,比较熟悉。
打开DEV自带的Demo,很容易就看到了word的演示,代码也很简单,如下图。
下一步要做的就是将这个组件和InkCanvas结合起来,通过点击某个按钮触发InkCanvas覆盖到文档上面,然后再使用某个按钮事件触发将InkCanvas关闭或者隐藏掉,同时获取InkCanvas的痕迹保存为图片,再插入到文档指定位置就好了。详细实现代码参考第4部分。
4 主要代码
先看看UI部分的布局,将InkCanvas嵌入到Dev的word文档组件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
< Grid x:Name = "mainGrid" > < dxre:RichEditControl x:Name = "richEditControl1" BarManager = "{Binding ElementName=barManager1, Mode=OneTime}" Ribbon="{Binding ElementName = ribbonControl1 , Mode = OneTime }" MouseMove = "richEditControl1_MouseMove" MouseLeave = "RichEditControl1_OnMouseLeave" MouseUp = "richEditControl1_MouseUp" /> < InkCanvas x:Name = "mainCanvas" Visibility = "Hidden" > < InkCanvas.DefaultDrawingAttributes > < DrawingAttributes Color = "Red" FitToCurve = "True" Height = "4" IgnorePressure = "False" IsHighlighter = "False" StylusTip = "Ellipse" StylusTipTransform = "Identity" Width = "4" /> </ InkCanvas.DefaultDrawingAttributes > < InkCanvas.Background > < SolidColorBrush Color = "White" Opacity = "0.01" /> </ InkCanvas.Background > </ InkCanvas > </ Grid > |
这一部分除了红框中的,其他的都是直接copy的DEV官方demo中的,红框的目的也是将手写板控件放置在文档内容页上面,默认是隐藏的,需要的时候再代码控制显示就好。
现在先弄俩按钮,“开始批注”和“停止批注”,来触发和停止墨迹功能。
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
< dxr:RibbonPage x:Name = "penPage" Caption = "批注" > < dxr:RibbonPageGroup x:Name = "penEvent" Caption = "画笔" ShowCaptionButton = "False" > < dxb:BarButtonItem x:Name = "startPenItem" LargeGlyph = "pack://application:,,,/Resources/pen.png" GlyphSize="L arge" Content = "开始批注" /> < dxb:BarButtonItem x:Name = "endPenItem" LargeGlyph = "pack://application:,,,/Resources/pen_red.png" GlyphSize=" Large" Content = "停止批注" IsEnabled = "False" /> < dxb:BarButtonItem x:Name = "eraserItem" LargeGlyph = "pack://application:,,,/Resources/eraser.png" GlyphSize=" Large" Content = "擦除" IsEnabled = "True" /> </ dxr:RibbonPageGroup > < dxr:RibbonPageGroup x:Name = "penSetting" Caption = "画笔设置" ShowCaptionButton = "False" > < dxre:BarSplitButtonColorEditItem x:Name = "penColor" Content = "画笔颜色" LargeGlyph="pack://application:,,, /Resources/pallet.png" RibbonStyle = "Large" IsEnabled = "False" > < dxb:PopupControlContainerInfo > < dxe:ColorEdit ChipSize = "Large" Name = "penColorEdit" ChipMargin = "5" ColumnCount = "5" ShowMoreColorsButton = "False" ShowDefaultColorButton = "False" ShowNoColorButton = "True" ShowBorder = "False" > < dxe:ColorEdit.Palettes > < dxre:CharactersBackgroundColorPaletteCollection /> </ dxe:ColorEdit.Palettes > </ dxe:ColorEdit > </ dxb:PopupControlContainerInfo > </ dxre:BarSplitButtonColorEditItem > < dxre:BarSplitButtonEditItem x:Name = "penLine" Content = "画笔线宽" LargeGlyph = "Resources/penline.png" IsEnab led = "False" > < dxb:PopupControlContainerInfo x:Name = "cccc" > < dxe:ListBoxEdit Name = "penLineEdit" ShowBorder = "False" ShowCustomItems = "False" > < dxe:ListBoxEditItem Content = "1" /> < dxe:ListBoxEditItem Content = "2" /> < dxe:ListBoxEditItem Content = "3" /> < dxe:ListBoxEditItem Content = "4" /> < dxe:ListBoxEditItem Content = "5" /> < dxe:ListBoxEditItem Content = "6" /> < dxe:ListBoxEditItem Content = "7" /> < dxe:ListBoxEditItem Content = "8" /> < dxe:ListBoxEditItem Content = "9" /> < dxe:ListBoxEditItem Content = "10" /> < dxe:ListBoxEditItem Content = "12" /> < dxe:ListBoxEditItem Content = "14" /> </ dxe:ListBoxEdit > </ dxb:PopupControlContainerInfo > </ dxre:BarSplitButtonEditItem > </ dxr:RibbonPageGroup > </ dxr:RibbonPage > |
这个直接在界面上面增加一个“dxr:RibbonPage ”就可以了,里面增加对应的按钮和一些设置项目。
下面是开始批注代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class StartPenCommand : System.Windows.Input.ICommand { public event EventHandler CanExecuteChanged; public bool CanExecute( object parameter) { return true ; } public void Execute( object parameter) { MainWindow window = parameter as MainWindow; window.StartPen( true ); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/// <summary> /// 开始批注 /// </summary> /// <param name="start"></param> public void StartPen( bool start) { this .richEditControl1.ActiveView.ZoomFactor = 1; startPenItem.IsEnabled = !start; endPenItem.IsEnabled = start; eraserItem.IsEnabled = !start; penColor.IsEnabled = start; penLine.IsEnabled = start; var cursor= new Cursor( new MemoryStream(Resource.MetroBusy)); if (!start) this .mainCanvas.Cursor = cursor; this .mainCanvas.Visibility = start ? Visibility.Visible : Visibility.Hidden; } |
停止批注代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class EndPenCommand : System.Windows.Input.ICommand { public event EventHandler CanExecuteChanged; public bool CanExecute( object parameter) { return true ; } public void Execute( object parameter) { MainWindow window = parameter as MainWindow; window.StartPen( false ); } } |
然后是一个监听,在InkCanvas痕迹绘制就在对应文档增加一笔痕迹。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//画笔绘制事件 this .mainCanvas.StrokeCollected += (s, o) => { Stroke item = new Stroke() { Color = lineColor, LineWidth = lineWidth, Points = new List< float []>() }; System.Windows.Ink.Stroke stroke = mainCanvas.Strokes[0]; for ( int i = 0; i < stroke.Clone().StylusPoints.Count; i++) { var pt = stroke.StylusPoints[i]; item.Points.Add( new float [2] { ( float )pt.X, ( float )pt.Y }); } mainCanvas.Strokes.Clear(); DrawStroke(item); }; |
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
|
/// <summary> /// 绘制痕迹 /// </summary> /// <param name="stroke"></param> private void DrawStroke(Stroke stroke) { if (stroke.Points.Count < 3) return ; float minX = float .MaxValue; float maxX = 0; float minY = float .MaxValue; float maxY = 0; float width = 0; float height = 0; for ( int i = 0; i < stroke.Points.Count; i++) { var pt = stroke.Points[i]; if (pt[0] < minX) minX = pt[0]; if (pt[0] > maxX) maxX = pt[0]; if (pt[1] < minY) minY = pt[1]; if (pt[1] > maxY) maxY = pt[1]; } width = maxX - minX; height = maxY - minY; Bitmap bmp = new Bitmap(( int )width + stroke.LineWidth+10, ( int )height + stroke.LineWidth+10); Graphics g = Graphics.FromImage(bmp); g.SmoothingMode = SmoothingMode.HighQuality; Pen pen = new Pen(System.Drawing.Color.FromArgb(255, stroke.Color.R, stroke.Color.G, stroke.Color.B), stroke.LineWidth); System.Drawing.PointF[] points = new System.Drawing.PointF[stroke.Points.Count]; for ( int i = 0; i < stroke.Points.Count; i++) { var pt = stroke.Points[i]; //var pt2 = stroke.Points[i + 1]; var x1 = ( float )(pt[0] - minX + stroke.LineWidth); var y1 = ( float )(pt[1] - minY + stroke.LineWidth); //var x2 = (float)(pt2[0] - minX); //var y2 = (float)(pt2[1] - minY); //g.DrawLine(pen, x1, y1, x2, y2); points[i] = ( new PointF(x1, y1)); } g.DrawCurve(pen, points); //标注起点坐标 var startPoint = this .mainCanvas.PointToScreen( new System.Windows.Point(minX, minY)); //标注起点坐标相对于文本控件的位置 var editPoint = this .richEditControl1.PointFromScreen(startPoint); //总页码 int pageCount = ((DevExpress.XtraRichEdit.PageBasedRichEditView)richEditControl1.ActiveView).PageCount; ////是否有分页符 //bool hasPageBlock = false; //for (int i = 0; i < pageCount; i++) //{ // LayoutPage layoutPage = this.richEditControl1.DocumentLayout.GetPage(i); // if (!hasPageBlock) // { // hasPageBlock = layoutPage.PageAreas.FirstOrDefault().Columns.Any(x => x.Rows.Any(y => y.Boxes.Any(z => z.Type == LayoutType.PageBreakBox))); // break; // } //} //获取当前可见页 var pageInfos = richEditControl1.ActiveView.GetVisiblePageLayoutInfos(); foreach (PageLayoutInfo pageInfo in pageInfos) { if (pageInfo.Bounds.Y > (-1) * pageInfo.Bounds.Height) { //画笔起点是否在当前可见页 if (editPoint.X >= pageInfo.Bounds.X && editPoint.X <= pageInfo.Bounds.X + pageInfo.Bounds.Width && editPoint.Y >= pageInfo.Bounds.Y && editPoint.Y <= pageInfo.Bounds.Y + pageInfo.Bounds.Height) { //当前可见页 int pageIndex = pageInfo.PageIndex; LayoutPage layoutPage = this .richEditControl1.DocumentLayout.GetPage(pageIndex); DocumentPosition documentPosition; if (layoutPage.MainContentRange.Length == 1 && pageIndex == pageCount - 1) { //最后一页,且是分页符空白页 documentPosition = this .richEditControl1.Document.CreatePosition( layoutPage.MainContentRange.Start + 1); DocumentRange documentRange = this .richEditControl1.Document.CreateRange(documentPosition, 1); this .richEditControl1.Document.InsertDocumentContent(documentPosition, documentRange); documentPosition = documentRange.End; this .richEditControl1.Document.Delete(documentRange); } else { documentPosition = this .richEditControl1.Document.CreatePosition(layoutPage.MainContentRange.Start + layoutPage.MainContentRange.Length / 2); } //if (layoutPage.PageAreas.FirstOrDefault().Columns.Any(x => x.Rows.Any(y => y.Boxes.Any(z => z.Type == LayoutType.PageBreakBox)))) //if (this.richEditControl1.Document.HtmlText.Contains("cs1B16EEB5")) /*if (hasPageBlock) { //有分页符 if (pageIndex >0) { LayoutPage preLayoutPage = this.richEditControl1.DocumentLayout.GetPage(pageIndex - 1); if ( !preLayoutPage.PageAreas.FirstOrDefault().Columns.Any(x =>x.Rows.Any(y => y.Boxes.Any(z => z.Type == LayoutType.PageBreakBox)))) { //前一页没有分页符 documentPosition = this.richEditControl1.Document.CreatePosition(layoutPage.MainContentRange.Start); } else { //前一页有分页符 documentPosition = this.richEditControl1.Document.CreatePosition( layoutPage.MainContentRange.Start); DocumentRange documentRange = this.richEditControl1.Document.CreateRange(documentPosition, 1); this.richEditControl1.Document.InsertDocumentContent(documentPosition, documentRange); documentPosition = documentRange.End; this.richEditControl1.Document.Delete(documentRange); } } else { documentPosition = this.richEditControl1.Document.CreatePosition(layoutPage.MainContentRange.Start); } //if (pageIndex == pageCount - 1) //{ // //if (!layoutPage.PageAreas.FirstOrDefault().Columns.Any(x => x.Rows.Any(y => y.Boxes.Any(z=>z.Type == LayoutType.ParagraphMarkBox)))) // //{ // // DocumentRange documentRange = this.richEditControl1.Document.CreateRange(documentPosition, 1); // // this.richEditControl1.Document.InsertDocumentContent(documentPosition, documentRange); // // documentPosition = documentRange.End; // // this.richEditControl1.Document.Delete(documentRange); // //} // documentPosition = // this.richEditControl1.Document.CreatePosition(layoutPage.MainContentRange.Start + // layoutPage.MainContentRange.Length); // DocumentRange documentRange = // this.richEditControl1.Document.CreateRange(documentPosition, 1); // this.richEditControl1.Document.InsertDocumentContent(documentPosition, documentRange); // documentPosition = documentRange.End; // this.richEditControl1.Document.Delete(documentRange); //} //else //{ // documentPosition = this.richEditControl1.Document.CreatePosition(layoutPage.MainContentRange.Start); //} } else { //无分页符 documentPosition = this.richEditControl1.Document.CreatePosition(layoutPage.MainContentRange.Start); }*/ var focusControl = richEditControl1.FocusElement; var controls = GetCanvasControls(focusControl); //根据相对位置坐标绘制图片 foreach (FrameworkElement box in controls) { Console.WriteLine(box.Name); if (box.Name == "SuperRoot" ) { if (box.ActualHeight == 0) { } else { Canvas canva = box as Canvas; var point = box.GetPosition( this .richEditControl1); if (point.Y > (-1) * canva.ActualHeight && point.Y < canva.ActualHeight) { try { System.Windows.Point gridPoint = canva.PointFromScreen(startPoint); if (gridPoint.Y < canva.ActualHeight && gridPoint.Y > 0 && gridPoint.X < canva.ActualWidth && gridPoint.X > 0) { Shape shape = this .richEditControl1.Document.InsertPicture(documentPosition, bmp); shape.TextWrapping = TextWrappingType.InFrontOfText; shape.HorizontalAlignment = ShapeHorizontalAlignment.None; //shape.RelativeHorizontalPosition = ShapeRelativeHorizontalPosition.Column; //shape.RelativeVerticalPosition = ShapeRelativeVerticalPosition.Paragraph; shape.ZOrder = zIndex; zIndex++; shape.Name = "penLink" ; shape.Offset = new PointF(( float )((gridPoint.X - stroke.LineWidth) * 3.12), ( float )((gridPoint.Y - stroke.LineWidth) * 3.12)); break ; } } catch (Exception ex) { } } } } } break ; } } } } |
5 总结
最终实现效果如下图。
源码和更多资源请访问:http://88gis.cn/web/pages/blog/blogInfo.html?id=95e5139d-5dcc-4600-bcca-355ae6ac8a8f
WPF实现WORD 2013墨迹批注功能的更多相关文章
- 【超详细教程】使用Windows Live Writer 2012和Office Word 2013 发布
去年就知道有这个功能,不过没去深究总结过,最近有写网络博客的欲望了,于是又重新拾起这玩意儿. 具体到底是用Windows Live Writer 2012还是用Word 2013,个人觉得看个人,因为 ...
- Word 2013双引号的BUG
相信使用Word 2013的朋友大多碰到过这样一个双引号的bug: 问题详细描述: word2013中,打字时引号出现问题,在输入中文情况下,输入左引号为中文,输入右引号时会自动变成英文.微软自己的输 ...
- [转载]Word直接发布新浪博客(以Word 2013为例)
原文地址:Word直接发布新浪博客(以Word 2013为例)作者:paulke2011 注意:这篇博客直接由Word 2013发出!这虽然也算是一个教程,但更多的是一个试验品. 老早就知道Word有 ...
- (7)Microsoft office Word 2013版本操作入门_常用技巧
1.自定义快速功能栏调整:常用功能按钮可以设置显示到此处.(如图所示的另存为和插入批注功能) 2.word中截图功能: 2.1 截图插入后的图片,可以进行设置 选中图片---点击[格式]可以设置图片 ...
- 使用 Word 2013 维护博客
博客的发布.修改是一件非常耗时.耗精力的事情.借助 Word 2013,维护博客将变得非常简单. 1 新建博客文章 运行 Word 2013,新建文档.如下图所示: 图1 鼠标左键单击上图的" ...
- Word 2013安裝字典
不必從內建的字典中開始,Word 2013 可將您連結到 Office 市集,方便您挑選免費的字典,或從包括多語字典的字典集合中購買. 若要選擇並安裝您想要的字典,請以滑鼠右鍵按一下任何單字,並按一下 ...
- 如何用 Windows Live Writer 和 Word 2013 分别发表博客到Cnblog 和CSDN
ps CSDN 老是505错误,放弃了 为什么会写这篇 最近写博客在 Cnblog 上面写博客, 发现图片不能复制了直接粘贴上,这对于把博客当随手笔记的人来说无疑非常痛苦.求助于博客园,他们让我用 W ...
- Azure认知服务之使用墨迹识别功能识别手写汉字
前面我们使用Azure Face实现了人脸识别.使用Azure表格识别器提取了表格里的数据.这次我们试试使用Azure墨迹识别API来对笔迹进行识别. 墨迹识别 墨迹识别器认知服务提供基于云的 RES ...
- 开始用Word 2013来写博客
第一步:如果从未发布过博客文章的话,需要在菜单里面选这里添加博客账号 第二步:选择正确的设置 第三步:写完博客之后,按这里就可以发布了! 如果以后需要写新的博客的话,还可以直接点这里: ...
随机推荐
- popupWindow自适应大小
// popupWindow自适应大小 popupWindow = new PopupWindow(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP ...
- [原创]使用OPENCC库进行简繁转换(C++代码)
最近公司有一款游戏产品,字库存在问题,希望全自动进行简繁同屏自动转换的行为,减少工作量. 所以自己使用了WINDOWS自带的一些转换函数,但发现大量字出现异常,无法转换(测试iconv也发现无法转换) ...
- Linux中逻辑卷(LVM)管理基本操作
1.创建逻辑卷 原文:https://linux.cn/article-3965-1.html
- Mysql命令行查看数据库大小(数据库版本为5.7以上)
数据库版本为5.7以上1.选择数据库use mydb1; 2.查看指定数据库表结构select * from information_schema.TABLES where information_s ...
- 日志文件(关于#IRSA_MDPS_RDM软件 密码登录事项 7月26号)
1.登录:sqlplus 用户名:scott 口令:123 qweas.. //2018-7-16号更改密码 2.查看该用户(已登录)下有几个表:select table_name from user ...
- 点击导航栏tableView回到顶部
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector ...
- TSQL--聚合函数
--======================================================== --COUNT --COUNT(1) 和COUNT(*) 计算结果相同,COUNT ...
- nginx停止
- Working Set
类似于Visual Studio中的Solution 如果Eclipse中的project过多,而且不是同一个真实的项目中的,可以按Working Set对project进行组织,只是一个逻辑组织 操 ...
- [uwp]数据绑定再学习
在开始上代码前,先来假设这样一种情形: 出于某些原因,创建一个自定义控件(UserControl),然后为它定义一个依赖属性,这个属性有两个作用,一是调用控件方通过数据绑定技术为它赋值,二是控件内部的 ...