1 前言

WORD 2013可以使用墨迹在文档上面标注,本文讲述通过WPF第三方控件实现类似主要功能如下:

名称描述
墨迹标注不论是否触摸屏环境下可以开始墨迹功能,并实现鼠标/触摸在文档任意位置绘制痕迹
墨迹痕迹保存绘制的墨迹能够完整在word中保存
墨迹参数设置设置墨迹的颜色和线条粗细
墨迹擦除提供擦除工具,可以擦除已经绘制的墨迹

2 环境及三方组件

名称描述
DevExpress V16.2使用其RichEditor控件作为word文档查看和编辑控件
Aspose.Word在保存word文档时,调整墨迹痕迹的一些属性
.NetFrameWork 4.5.net运行库环境

3 实现思路

首先来看一下word中如何开启墨迹。这个就要说微软不地道了,微软规定了只有在触摸屏的windows环境下才会默认显示墨迹按钮,使用鼠标的就默认不显示,当然我们也可以在选项中强制加上,然后没有啥用,因为使用鼠标的用户会发现“开始墨迹书写”按钮是灰色的,而触屏下是可用的,如下图。

WPF实现WORD 2013墨迹批注功能-LMLPHP

我们先摒弃这个梗不说,为什么要自己开发一个墨迹功能呢,可能是因为office太贵了,一般客户买不起或者不想买,于是就要开发一个包含word基本功能的替代品,也有可能是我们开发软性时候需要集成word类似的功能,或者其他种种原因,所以还是决定自己开发(模仿)一个word的墨迹功能。废话少说,下面来一步一步的分析和设计墨迹功能实现思路。

首先,我找到一个在触摸屏上面写好一些文字的文档,在非触摸屏电脑上打开,选中墨迹,这时候工具栏中“图片”选项卡被激活了,可见所谓的“墨迹”实际上只是将手写痕迹保存为类似图片存储起来了,如下图。

WPF实现WORD 2013墨迹批注功能-LMLPHP

WPF实现WORD 2013墨迹批注功能-LMLPHP

总结一下,墨迹保存为图片的主要属性有这样:

1.允许重叠(好像默认插入图片是不允许重叠的)

2.绝对位置(应该是左上角的坐标位置吧)

3.浮于文字上方(这个肯定的,避免扰乱文字布局)

有了这些属性,我大胆设想如果我使用电子手写板插件,将手写痕迹保存下来,然后赋予这些属性,应该也可以达到墨迹的效果。

现在到了框架选型的阶段,首先我需要一个能够打开和编辑word的UI组件,然后我还需要一个电子手写板插件,最后将他们组合起来。这里我选择了DEV V1.6.2 的WPF版本和WPF的InkCanvas组件,这俩我都在实际中用过,比较熟悉。

打开DEV自带的Demo,很容易就看到了word的演示,代码也很简单,如下图。

WPF实现WORD 2013墨迹批注功能-LMLPHP

下一步要做的就是将这个组件和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=ribbonControl1Mode=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>

WPF实现WORD 2013墨迹批注功能-LMLPHP

这一部分除了红框中的,其他的都是直接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 总结

最终实现效果如下图。

WPF实现WORD 2013墨迹批注功能-LMLPHP

源码和更多资源请访问:http://88gis.cn/web/pages/blog/blogInfo.html?id=95e5139d-5dcc-4600-bcca-355ae6ac8a8f

05-08 08:15