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