├── .gitignore ├── AngleDrawTool.cs ├── App.config ├── App.xaml ├── App.xaml.cs ├── AreaDrawTool.cs ├── ArrowDrawTool.cs ├── Attach └── IconElement.cs ├── ClosedCurveDrawTool.cs ├── CurveDrawTool.cs ├── DrawCursors.cs ├── DrawGeometryBase.cs ├── DrawToolType.cs ├── DrawTools.csproj ├── DrawTools.sln ├── DrawingCanvas.cs ├── DrawingCanvasViewer.cs ├── EllipseDrawTool.cs ├── EraserDrawTool.cs ├── IDrawTool.cs ├── Images ├── Cursor │ └── hand.png ├── angle1.png ├── angle2.png ├── area1.png ├── area2.png ├── arrow1.png ├── arrow2.png ├── circle1.png ├── circle2.png ├── clear1.png ├── clear2.png ├── closedCurve1.png ├── closedCurve2.png ├── colors1.png ├── colors2.png ├── curve1.png ├── curve2.png ├── ellipse1.png ├── ellipse2.png ├── eraser1.png ├── eraser2.png ├── img1.jpg ├── line1.png ├── line2.png ├── pen1.png ├── pen2.png ├── pointer1.png ├── pointer2.png ├── polyline1.png ├── polyline2.png ├── ranging1.png ├── ranging2.png ├── rectangle1.png ├── rectangle2.png ├── text1.png └── text2.png ├── LICENSE ├── LineDrawTool.cs ├── PenDrawTool.cs ├── PointerDrawTool.cs ├── PolygonDrawTool.cs ├── PolylineDrawTool.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings ├── RangingDrawTool.cs ├── RectangleDrawTool.cs ├── Serialize ├── DrawAngleSerializer.cs ├── DrawAreaSerializer.cs ├── DrawArrowSerializer.cs ├── DrawClosedCurveSerializer.cs ├── DrawCurveSerializer.cs ├── DrawEllipseSerializer.cs ├── DrawGeometrySerializer.cs ├── DrawGeometrySerializerBase.cs ├── DrawLineSerializer.cs ├── DrawPenSerializer.cs ├── DrawPolygonSerializer.cs ├── DrawPolylineSerializer.cs ├── DrawRangingSerializer.cs ├── DrawRectangleSerializer.cs └── DrawTextSerializer.cs ├── TextDrawTool.cs ├── Themes └── generic.xaml ├── Utils ├── DpiHelper.cs ├── ImageHelper.cs ├── NativeMethods.cs └── TreeHelper.cs ├── Views ├── MainWindow.xaml └── MainWindow.xaml.cs └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /AngleDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | 6 | namespace DrawTools 7 | { 8 | /// 9 | /// 角度 10 | /// 11 | public sealed class AngleDrawTool : DrawGeometryBase 12 | { 13 | public AngleDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 14 | { 15 | this.DrawingToolType = DrawToolType.Angle; 16 | 17 | // 准备要处理的事件 18 | this.CanTouchDown = true; 19 | } 20 | 21 | #region 鼠标键盘事件 22 | 23 | public override Boolean OnTouchLeave(Point point) 24 | { 25 | if (formattedText == null) 26 | this.drawingCanvas.DeleteVisual(this); 27 | else 28 | { 29 | var textGeometry = formattedText.BuildGeometry(textPoint); 30 | textGeometry.Transform = new RotateTransform(textAngle, textCenterPoint.X, textCenterPoint.Y); 31 | geometry = geometry.GetWidenedPathGeometry(pen); 32 | geometry = Geometry.Combine(geometry, textGeometry, GeometryCombineMode.Union, null); 33 | 34 | Draw(); 35 | } 36 | 37 | this.drawingCanvas.DeleteWorkingDrawTool(this); 38 | 39 | this.IsFinish = true; 40 | 41 | this.CanTouchDown = false; 42 | this.CanTouchMove = false; 43 | this.CanTouchLeave = false; 44 | 45 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 46 | this.drawingCanvas.ReleaseMouseCapture(); 47 | 48 | return true; 49 | } 50 | 51 | public override Boolean OnTouchDown(Int32 touchId, Point point) 52 | { 53 | this.TouchId = touchId; 54 | 55 | if (!vertex.HasValue) 56 | { 57 | this.drawingCanvas.AddWorkingDrawTool(this); 58 | 59 | this.pen = this.drawingCanvas.Pen; 60 | this.minWidth = this.pen.Thickness * 20; 61 | this.arcLen = this.pen.Thickness * 15; 62 | this.arcTextLen = this.pen.Thickness * 25; 63 | 64 | this.fontSize = this.drawingCanvas.FontSize; 65 | this.typeface = new Typeface(new FontFamily("Microsoft YaHei UI,Tahoma"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); 66 | 67 | vertex = point; 68 | 69 | geometry = new PathGeometry(); 70 | 71 | var figure = new PathFigure { StartPoint = point }; 72 | pathGeometry.Figures.Add(figure); 73 | 74 | this.CanTouchMove = true; 75 | 76 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 77 | this.CanTouchLeave = true; 78 | 79 | this.drawingCanvas.AddVisual(this); 80 | } 81 | else if ((point - vertex.Value).Length > minWidth && !startPoint.HasValue) 82 | { 83 | startPoint = point; 84 | 85 | var figure = pathGeometry.Figures[0]; 86 | figure.StartPoint = point; 87 | 88 | points = new Point[2]; 89 | points[0] = vertex.Value; 90 | 91 | var line = new LineSegment(vertex.Value, true) { IsSmoothJoin = true }; 92 | 93 | if (mousePoint.HasValue) 94 | { 95 | figure.Segments[figure.Segments.Count - 1] = line; 96 | mousePoint = null; 97 | } 98 | else 99 | figure.Segments.Add(line); 100 | 101 | var dc = this.RenderOpen(); 102 | dc.DrawGeometry(null, pen, geometry); 103 | dc.Close(); 104 | 105 | if (point.X == vertex.Value.X) 106 | { 107 | arcPoint1.X = arcTextPoint1.X = point.X; 108 | arcPoint1.Y = vertex.Value.Y + (point.Y > vertex.Value.Y ? arcLen : -arcLen); 109 | arcTextPoint1.Y = vertex.Value.Y + (point.Y > vertex.Value.Y ? arcTextLen : -arcTextLen); 110 | } 111 | else 112 | { 113 | k1 = (point.Y - vertex.Value.Y) / (point.X - vertex.Value.X); 114 | 115 | var offsetX = Math.Sqrt(arcLen * arcLen / (k1.Value * k1.Value + 1)) * (point.X > vertex.Value.X ? 1 : -1); 116 | 117 | arcPoint1.X = vertex.Value.X + offsetX; 118 | arcPoint1.Y = vertex.Value.Y + offsetX * k1.Value; 119 | 120 | offsetX = Math.Sqrt(arcTextLen * arcTextLen / (k1.Value * k1.Value + 1)) * (point.X > vertex.Value.X ? 1 : -1); 121 | 122 | arcTextPoint1.X = vertex.Value.X + offsetX; 123 | arcTextPoint1.Y = vertex.Value.Y + offsetX * k1.Value; 124 | } 125 | 126 | figure = new PathFigure { StartPoint = arcPoint1 }; 127 | pathGeometry.Figures.Add(figure); 128 | 129 | figure = new PathFigure { StartPoint = vertex.Value }; 130 | pathGeometry.Figures.Add(figure); 131 | } 132 | else 133 | return OnTouchLeave(point); 134 | 135 | return true; 136 | } 137 | 138 | public override Boolean OnTouchMove(Point point) 139 | { 140 | if (!startPoint.HasValue) 141 | { 142 | // 画线 143 | var dc = this.RenderOpen(); 144 | 145 | if ((point - vertex.Value).Length < minWidth) 146 | { 147 | dc.Close(); 148 | return true; 149 | } 150 | 151 | var figure = pathGeometry.Figures[0]; 152 | var line = new LineSegment(point, true) { IsSmoothJoin = true }; 153 | 154 | if (mousePoint.HasValue) 155 | figure.Segments[figure.Segments.Count - 1] = line; 156 | else 157 | figure.Segments.Add(line); 158 | 159 | mousePoint = point; 160 | 161 | dc.DrawGeometry(null, pen, geometry); 162 | dc.Close(); 163 | } 164 | else 165 | { 166 | // 画夹角 167 | if ((point - vertex.Value).Length < minWidth) 168 | return true; 169 | 170 | mousePoint = points[1] = point; 171 | 172 | var polyLine = new PolyLineSegment(points, true); 173 | 174 | var figure = pathGeometry.Figures[0]; 175 | figure.Segments[figure.Segments.Count - 1] = polyLine; 176 | 177 | // 画圆弧 178 | if (point.X == vertex.Value.X) 179 | { 180 | arcPoint2.X = arcTextPoint2.X = point.X; 181 | arcPoint2.Y = vertex.Value.Y + (point.Y > vertex.Value.Y ? arcLen : -arcLen); 182 | arcTextPoint2.Y = vertex.Value.Y + (point.Y > vertex.Value.Y ? arcTextLen : -arcTextLen); 183 | } 184 | else 185 | { 186 | k2 = (point.Y - vertex.Value.Y) / (point.X - vertex.Value.X); 187 | 188 | var offsetX = Math.Sqrt(arcLen * arcLen / (k2.Value * k2.Value + 1)) * (point.X > vertex.Value.X ? 1 : -1); 189 | 190 | arcPoint2.X = vertex.Value.X + offsetX; 191 | arcPoint2.Y = vertex.Value.Y + offsetX * k2.Value; 192 | 193 | offsetX = Math.Sqrt(arcTextLen * arcTextLen / (k2.Value * k2.Value + 1)) * (point.X > vertex.Value.X ? 1 : -1); 194 | 195 | arcTextPoint2.X = vertex.Value.X + offsetX; 196 | arcTextPoint2.Y = vertex.Value.Y + offsetX * k2.Value; 197 | } 198 | 199 | if (k1.HasValue) 200 | { 201 | angle1 = Math.Atan(k1.Value) / Math.PI * 180; 202 | 203 | if (arcPoint1.X < vertex.Value.X) 204 | angle1 += 180; 205 | else if (k1.Value < 0) 206 | angle1 += 360; 207 | } 208 | else 209 | angle1 = arcPoint1.Y > vertex.Value.Y ? 90 : -90; 210 | 211 | if (k2.HasValue) 212 | { 213 | angle2 = Math.Atan(k2.Value) / Math.PI * 180; 214 | 215 | if (arcPoint2.X < vertex.Value.X) 216 | angle2 += 180; 217 | else if (k2.Value < 0) 218 | angle2 += 360; 219 | } 220 | else 221 | angle2 = arcPoint2.Y > vertex.Value.Y ? 90 : -90; 222 | 223 | angle = (angle2 + 360 - angle1) % 360; 224 | var clockwise = angle < 180; 225 | var arc = new ArcSegment(arcPoint2, new Size(arcLen, arcLen), 0, false, clockwise ? SweepDirection.Clockwise : SweepDirection.Counterclockwise, true); 226 | 227 | figure = pathGeometry.Figures[1]; 228 | 229 | if (figure.Segments.Count == 0) 230 | figure.Segments.Add(arc); 231 | else 232 | figure.Segments[0] = arc; 233 | 234 | var dc = this.RenderOpen(); 235 | 236 | // 画文字 237 | angle %= 180; 238 | 239 | if (!clockwise) 240 | angle = 180 - angle; 241 | 242 | var text = angle.ToString("0.00") + "°"; 243 | 244 | formattedText = new FormattedText( 245 | text, 246 | System.Globalization.CultureInfo.InvariantCulture, 247 | FlowDirection.LeftToRight, 248 | typeface, 249 | this.fontSize, 250 | pen.Brush); 251 | 252 | textCenterPoint.X = (arcTextPoint1.X + arcTextPoint2.X) / 2; 253 | textCenterPoint.Y = (arcTextPoint1.Y + arcTextPoint2.Y) / 2; 254 | 255 | if (textCenterPoint.X == vertex.Value.X) 256 | { 257 | if (textCenterPoint.Y >= vertex.Value.Y) 258 | { 259 | textAngle = 90; 260 | textCenterPoint.Y = vertex.Value.Y + arcTextLen; 261 | } 262 | else 263 | { 264 | textAngle = -90; 265 | textCenterPoint.Y = vertex.Value.Y - arcTextLen; 266 | } 267 | } 268 | else 269 | { 270 | k3 = (textCenterPoint.Y - vertex.Value.Y) / (textCenterPoint.X - vertex.Value.X); 271 | 272 | textAngle = Math.Atan(k3.Value) / Math.PI * 180; 273 | 274 | 275 | 276 | var offsetX = Math.Sqrt(arcTextLen * arcTextLen / (k3.Value * k3.Value + 1)) * (textCenterPoint.X > vertex.Value.X ? 1 : -1); 277 | 278 | textCenterPoint.X = vertex.Value.X + offsetX; 279 | textCenterPoint.Y = vertex.Value.Y + offsetX * k3.Value; 280 | } 281 | 282 | figure = pathGeometry.Figures[2]; 283 | var line = new LineSegment(textCenterPoint, true); 284 | 285 | if (figure.Segments.Count == 0) 286 | figure.Segments.Add(line); 287 | else 288 | figure.Segments[0] = line; 289 | 290 | dc.DrawGeometry(null, pen, geometry); 291 | 292 | dc.PushTransform(new RotateTransform(textAngle, textCenterPoint.X, textCenterPoint.Y)); 293 | 294 | var _textAngle = textAngle; 295 | 296 | textPoint.X = textCenterPoint.X; 297 | textPoint.Y = textCenterPoint.Y - fontSize / 2; 298 | 299 | if (textPoint.X < vertex.Value.X) 300 | _textAngle += 180; 301 | else if (k3.Value < 0) 302 | _textAngle += 360; 303 | 304 | if (_textAngle >= 90 && _textAngle <= 270) 305 | { 306 | var width = text.Length * fontSize / 2 + fontSize; 307 | textPoint.X -= width; 308 | } 309 | else 310 | textPoint.X += fontSize / 2; 311 | 312 | dc.DrawText(formattedText, textPoint); 313 | dc.Pop(); 314 | 315 | dc.Close(); 316 | } 317 | 318 | return true; 319 | } 320 | 321 | #endregion 322 | 323 | #region 序列化 324 | 325 | public override DrawGeometrySerializerBase ToSerializer() 326 | { 327 | var serializer = new DrawAngleSerializer 328 | { 329 | Color = ((SolidColorBrush)pen.Brush).Color, 330 | StrokeThickness = pen.Thickness, 331 | Geometry = geometry.ToString() 332 | }; 333 | 334 | if (geometry.Transform != null) 335 | serializer.Matrix = geometry.Transform.Value; 336 | 337 | return serializer; 338 | } 339 | 340 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 341 | { 342 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 343 | 344 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 345 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 346 | 347 | this.IsFinish = true; 348 | 349 | this.Draw(); 350 | } 351 | 352 | #endregion 353 | 354 | #region 字段 355 | 356 | private Point? vertex, startPoint, mousePoint; // 顶点、开始描点、鼠标移动点 357 | private Point[] points; // 角度连线 358 | private Point arcPoint1, arcPoint2; // 圆弧起始点 359 | private Point arcTextPoint1, arcTextPoint2, textCenterPoint, textPoint; // 文字圆弧起始点,及中点 360 | private Double? k1, k2, k3; // 两直线斜率,及文字斜率 361 | private Double angle, angle1, angle2, arcLen; // 两直线夹角, 及圆弧半径, 362 | private Double textAngle, arcTextLen; // 文字夹角,文字圆弧半径 363 | private Double minWidth; 364 | private Double fontSize; 365 | private Typeface typeface; 366 | private FormattedText formattedText; 367 | 368 | #endregion 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | 9 | namespace DrawTools 10 | { 11 | /// 12 | /// App.xaml 的交互逻辑 13 | /// 14 | public partial class App : Application 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AreaDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using DrawTools.Utils; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Windows; 7 | using System.Windows.Media; 8 | 9 | namespace DrawTools 10 | { 11 | /// 12 | /// 面积 13 | /// 14 | public sealed class AreaDrawTool : DrawGeometryBase 15 | { 16 | public AreaDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 17 | { 18 | this.DrawingToolType = DrawToolType.Area; 19 | 20 | // 准备要处理的事件 21 | this.CanTouchDown = true; 22 | } 23 | 24 | #region 鼠标键盘事件 25 | 26 | public override Boolean OnTouchLeave(Point point) 27 | { 28 | if (mousePoint.HasValue) 29 | { 30 | points.Add(mousePoint.Value); 31 | mousePoint = null; 32 | } 33 | 34 | if (area == 0) 35 | this.drawingCanvas.DeleteVisual(this); 36 | else 37 | { 38 | var textGeometry = formattedText.BuildGeometry(textPoint); 39 | geometry = geometry.GetWidenedPathGeometry(pen); 40 | geometry = Geometry.Combine(geometry, textGeometry, GeometryCombineMode.Union, null); 41 | 42 | Draw(); 43 | } 44 | 45 | this.drawingCanvas.DeleteWorkingDrawTool(this); 46 | 47 | this.IsFinish = true; 48 | 49 | this.CanTouchDown = false; 50 | this.CanTouchMove = false; 51 | this.CanTouchLeave = false; 52 | 53 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 54 | this.drawingCanvas.ReleaseMouseCapture(); 55 | 56 | return true; 57 | } 58 | 59 | public override Boolean OnTouchDown(Int32 touchId, Point point) 60 | { 61 | this.TouchId = touchId; 62 | 63 | if (points.Count == 0) 64 | { 65 | this.drawingCanvas.AddWorkingDrawTool(this); 66 | 67 | this.pen = this.drawingCanvas.Pen; 68 | this.fontSize = this.drawingCanvas.FontSize; 69 | this.typeface = new Typeface(new FontFamily("Microsoft YaHei UI,Tahoma"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); 70 | 71 | this.dpi = DpiHelper.GetDpiFromVisual(this.drawingCanvas); 72 | 73 | points.Add(point); 74 | 75 | geometry = new PathGeometry(); 76 | 77 | var figure = new PathFigure { StartPoint = point }; 78 | pathGeometry.Figures.Add(figure); 79 | 80 | this.CanTouchMove = true; 81 | 82 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 83 | this.CanTouchLeave = true; 84 | 85 | this.drawingCanvas.AddVisual(this); 86 | } 87 | else if ((point - points.Last()).Length <= pen.Thickness) 88 | return OnTouchLeave(point); 89 | else if (mousePoint.HasValue) 90 | { 91 | points.Add(mousePoint.Value); 92 | mousePoint = null; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | public override Boolean OnTouchMove(Point point) 99 | { 100 | if ((point - points.Last()).Length <= pen.Thickness) 101 | return true; 102 | 103 | var figure = pathGeometry.Figures[0]; 104 | var line = new LineSegment(point, true) { IsSmoothJoin = true }; 105 | 106 | if (mousePoint.HasValue) 107 | figure.Segments[figure.Segments.Count - 1] = line; 108 | else 109 | figure.Segments.Add(line); 110 | 111 | mousePoint = point; 112 | 113 | figure.IsClosed = figure.Segments.Count >= 2; 114 | 115 | var dc = this.RenderOpen(); 116 | 117 | dc.DrawGeometry(null, pen, geometry); 118 | 119 | if (figure.IsClosed) 120 | { 121 | area = geometry.GetArea(); 122 | 123 | if (area != 0) 124 | { 125 | area = area / Dpi.Cm2Wpf / Dpi.Cm2Wpf * 100; 126 | 127 | var text = area.ToString("0.00") + "mm²"; 128 | 129 | formattedText = new FormattedText( 130 | text, 131 | System.Globalization.CultureInfo.InvariantCulture, 132 | FlowDirection.LeftToRight, 133 | typeface, 134 | this.fontSize, 135 | pen.Brush); 136 | 137 | var width = text.Length * fontSize / 2; 138 | 139 | textPoint.X = (points.Sum(q => q.X) + point.X) / (points.Count + 1) - width / 2; 140 | textPoint.Y = (points.Sum(q => q.Y) + point.Y) / (points.Count + 1) - fontSize / 2; 141 | 142 | dc.DrawText(formattedText, textPoint); 143 | } 144 | } 145 | 146 | dc.Close(); 147 | 148 | return true; 149 | } 150 | 151 | #endregion 152 | 153 | #region 序列化 154 | 155 | public override DrawGeometrySerializerBase ToSerializer() 156 | { 157 | var serializer = new DrawAreaSerializer 158 | { 159 | Color = ((SolidColorBrush)pen.Brush).Color, 160 | StrokeThickness = pen.Thickness, 161 | Geometry = geometry.ToString() 162 | }; 163 | 164 | if (geometry.Transform != null) 165 | serializer.Matrix = geometry.Transform.Value; 166 | 167 | return serializer; 168 | } 169 | 170 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 171 | { 172 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 173 | 174 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 175 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 176 | 177 | this.IsFinish = true; 178 | 179 | this.Draw(); 180 | } 181 | 182 | #endregion 183 | 184 | #region 字段 185 | 186 | private List points = new List(); 187 | private Point? mousePoint; 188 | private Point textPoint; 189 | private Double fontSize; 190 | private Typeface typeface; 191 | private Double area; 192 | private FormattedText formattedText; 193 | private Dpi dpi; 194 | 195 | #endregion 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /ArrowDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | 6 | namespace DrawTools 7 | { 8 | /// 9 | /// 箭头 10 | /// 11 | public sealed class ArrowDrawTool : DrawGeometryBase 12 | { 13 | public ArrowDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 14 | { 15 | this.DrawingToolType = DrawToolType.Arrow; 16 | 17 | // 准备要处理的事件 18 | this.CanTouchDown = true; 19 | } 20 | 21 | #region 鼠标键盘事件 22 | 23 | public override Boolean OnTouchLeave(Point point) 24 | { 25 | if (!endPoint.HasValue) 26 | this.drawingCanvas.DeleteVisual(this); 27 | else 28 | { 29 | geometry = geometry.GetWidenedPathGeometry(pen); 30 | Draw(); 31 | } 32 | 33 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 34 | this.drawingCanvas.ReleaseMouseCapture(); 35 | 36 | this.drawingCanvas.DeleteWorkingDrawTool(this); 37 | 38 | this.IsFinish = true; 39 | 40 | this.CanKeyDown = false; 41 | this.CanTouchMove = false; 42 | this.CanTouchLeave = false; 43 | 44 | return true; 45 | } 46 | 47 | public override Boolean OnTouchDown(Int32 touchId, Point point) 48 | { 49 | this.TouchId = touchId; 50 | 51 | if (!startPoint.HasValue) 52 | { 53 | this.drawingCanvas.AddWorkingDrawTool(this); 54 | 55 | this.pen = this.drawingCanvas.Pen; 56 | 57 | startPoint = point; 58 | 59 | geometry = new PathGeometry(); 60 | 61 | var figure = new PathFigure { StartPoint = point }; 62 | pathGeometry.Figures.Add(figure); 63 | figure = new PathFigure(); 64 | pathGeometry.Figures.Add(figure); 65 | 66 | arrowPoints = new Point[2]; 67 | 68 | this.CanTouchMove = true; 69 | 70 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 71 | this.CanTouchLeave = true; 72 | 73 | this.drawingCanvas.AddVisual(this); 74 | 75 | return true; 76 | } 77 | else 78 | return OnTouchLeave(point); 79 | } 80 | 81 | public override Boolean OnTouchMove(Point point) 82 | { 83 | var dc = this.RenderOpen(); 84 | var start = startPoint.Value; 85 | 86 | if ((point - start).Length <= pen.Thickness * 6) 87 | { 88 | dc.Close(); 89 | return true; 90 | } 91 | 92 | var figure = pathGeometry.Figures[0]; 93 | 94 | var line = new LineSegment(point, true); 95 | 96 | if (endPoint.HasValue) 97 | figure.Segments[0] = line; 98 | else 99 | figure.Segments.Add(line); 100 | 101 | figure = pathGeometry.Figures[1]; 102 | 103 | arrowPoints[0] = point; 104 | Double? k = null; // 斜率 105 | var len = pen.Thickness * 6; 106 | 107 | if (start.X != point.X) 108 | k = (point.Y - start.Y) / (point.X - start.X); 109 | 110 | if (k.HasValue) 111 | { 112 | var angle = Math.Atan(k.Value) / Math.PI * 180; 113 | var center = new Point(); 114 | var offsetX = Math.Sqrt(len * len / (k.Value * k.Value + 1)) * (point.X > start.X ? -1 : 1); 115 | 116 | center.X = point.X + offsetX; 117 | center.Y = point.Y + offsetX * k.Value; 118 | 119 | len /= 2; 120 | k = -1 / k; 121 | 122 | offsetX = Math.Sqrt(len * len / (k.Value * k.Value + 1)) * (angle > 0 ? 1 : -1); 123 | 124 | arrowStartPoint.X = center.X + offsetX; 125 | arrowStartPoint.Y = center.Y + offsetX * k.Value; 126 | 127 | arrowPoints[1].X = center.X - offsetX; 128 | arrowPoints[1].Y = center.Y - offsetX * k.Value; 129 | } 130 | else 131 | { 132 | if (start.Y > point.Y) 133 | { 134 | // 箭头向上 135 | arrowStartPoint.X = point.X - len / 2; 136 | arrowPoints[1].X = point.X + len / 2; 137 | arrowStartPoint.Y = arrowPoints[1].Y = point.Y + len; 138 | } 139 | else 140 | { 141 | arrowStartPoint.X = point.X + len / 2; 142 | arrowPoints[1].X = point.X - len / 2; 143 | arrowStartPoint.Y = arrowPoints[1].Y = point.Y - len; 144 | } 145 | } 146 | 147 | figure.StartPoint = arrowStartPoint; 148 | var polyLine = new PolyLineSegment(arrowPoints, true); 149 | 150 | if (endPoint.HasValue) 151 | figure.Segments[0] = polyLine; 152 | else 153 | figure.Segments.Add(polyLine); 154 | 155 | endPoint = point; 156 | 157 | dc.DrawGeometry(null, pen, geometry); 158 | dc.Close(); 159 | 160 | return true; 161 | } 162 | 163 | #endregion 164 | 165 | #region 序列化 166 | 167 | public override DrawGeometrySerializerBase ToSerializer() 168 | { 169 | var serializer = new DrawArrowSerializer 170 | { 171 | Color = ((SolidColorBrush)pen.Brush).Color, 172 | StrokeThickness = pen.Thickness, 173 | Geometry = geometry.ToString() 174 | }; 175 | 176 | if (geometry.Transform != null) 177 | serializer.Matrix = geometry.Transform.Value; 178 | 179 | return serializer; 180 | } 181 | 182 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 183 | { 184 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 185 | 186 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 187 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 188 | 189 | this.IsFinish = true; 190 | 191 | this.Draw(); 192 | } 193 | 194 | #endregion 195 | 196 | #region 字段 197 | 198 | private Point? startPoint, endPoint; 199 | private Point arrowStartPoint; 200 | private Point[] arrowPoints; 201 | 202 | #endregion 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Attach/IconElement.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Media; 3 | 4 | namespace DrawTools.Attach 5 | { 6 | public static class IconElement 7 | { 8 | public static readonly DependencyProperty ImageProperty = DependencyProperty.RegisterAttached( 9 | "Image", typeof(ImageSource), typeof(IconElement)); 10 | 11 | public static void SetImage(DependencyObject element, ImageSource value) => element.SetValue(ImageProperty, value); 12 | 13 | public static ImageSource GetImage(DependencyObject element) => (ImageSource)element.GetValue(ImageProperty); 14 | 15 | public static readonly DependencyProperty ImageSelectedProperty = DependencyProperty.RegisterAttached( 16 | "ImageSelected", typeof(ImageSource), typeof(IconElement)); 17 | 18 | public static void SetImageSelected(DependencyObject element, ImageSource value) => element.SetValue(ImageSelectedProperty, value); 19 | 20 | public static ImageSource GetImageSelected(DependencyObject element) => (ImageSource)element.GetValue(ImageSelectedProperty); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ClosedCurveDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | 8 | namespace DrawTools 9 | { 10 | /// 11 | /// 曲线 12 | /// 13 | public sealed class ClosedCurveDrawTool : DrawGeometryBase 14 | { 15 | public ClosedCurveDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 16 | { 17 | this.DrawingToolType = DrawToolType.ClosedCurve; 18 | 19 | // 准备要处理的事件 20 | this.CanTouchDown = true; 21 | } 22 | 23 | #region 鼠标键盘事件 24 | 25 | public override Boolean OnTouchLeave(Point point) 26 | { 27 | var figure = pathGeometry.Figures[0]; 28 | figure.IsClosed = true; 29 | 30 | if (mousePoint.HasValue) 31 | { 32 | points.Add(mousePoint.Value); 33 | mousePoint = null; 34 | } 35 | 36 | point = points.Last(); 37 | var first = points[0]; 38 | 39 | if ((point - first).Length > pen.Thickness) 40 | { 41 | var centerX = (first.X + point.X) / 2; 42 | var bezier = new BezierSegment(new Point(centerX, point.Y), new Point(centerX, first.Y), first, true) { IsSmoothJoin = true }; 43 | 44 | if (mousePoint.HasValue) 45 | figure.Segments[figure.Segments.Count - 1] = bezier; 46 | else 47 | { 48 | figure.Segments.Add(bezier); 49 | mousePoint = null; 50 | } 51 | } 52 | 53 | if (points.Count < 3) 54 | this.drawingCanvas.DeleteVisual(this); 55 | else 56 | { 57 | geometry = geometry.GetWidenedPathGeometry(pen); 58 | Draw(); 59 | } 60 | 61 | this.drawingCanvas.DeleteWorkingDrawTool(this); 62 | 63 | this.IsFinish = true; 64 | 65 | this.CanTouchDown = false; 66 | this.CanTouchMove = false; 67 | this.CanTouchLeave = false; 68 | 69 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 70 | this.drawingCanvas.ReleaseMouseCapture(); 71 | 72 | return true; 73 | } 74 | 75 | public override Boolean OnTouchDown(Int32 touchId, Point point) 76 | { 77 | this.TouchId = touchId; 78 | 79 | if (points.Count == 0) 80 | { 81 | this.drawingCanvas.AddWorkingDrawTool(this); 82 | 83 | this.pen = this.drawingCanvas.Pen; 84 | 85 | points.Add(point); 86 | 87 | geometry = new PathGeometry(); 88 | 89 | var figure = new PathFigure { StartPoint = point }; 90 | pathGeometry.Figures.Add(figure); 91 | 92 | this.CanTouchMove = true; 93 | 94 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 95 | this.CanTouchLeave = true; 96 | 97 | this.drawingCanvas.AddVisual(this); 98 | } 99 | else if ((point - points.Last()).Length <= pen.Thickness) 100 | return OnTouchLeave(point); 101 | else if (mousePoint.HasValue) 102 | { 103 | points.Add(mousePoint.Value); 104 | mousePoint = null; 105 | } 106 | 107 | return true; 108 | } 109 | 110 | public override Boolean OnTouchMove(Point point) 111 | { 112 | var last = points.Last(); 113 | 114 | if ((point - last).Length <= pen.Thickness) 115 | return true; 116 | 117 | var figure = pathGeometry.Figures[0]; 118 | var centerX = (last.X + point.X) / 2; 119 | var bezier = new BezierSegment(new Point(centerX, last.Y), new Point(centerX, point.Y), point, true) { IsSmoothJoin = true }; 120 | 121 | if (mousePoint.HasValue) 122 | figure.Segments[figure.Segments.Count - 1] = bezier; 123 | else 124 | figure.Segments.Add(bezier); 125 | 126 | mousePoint = point; 127 | 128 | var dc = this.RenderOpen(); 129 | dc.DrawGeometry(null, pen, geometry); 130 | dc.Close(); 131 | 132 | return true; 133 | } 134 | 135 | #endregion 136 | 137 | #region 序列化 138 | 139 | public override DrawGeometrySerializerBase ToSerializer() 140 | { 141 | var serializer = new DrawClosedCurveSerializer 142 | { 143 | Color = ((SolidColorBrush)pen.Brush).Color, 144 | StrokeThickness = pen.Thickness, 145 | Geometry = geometry.ToString() 146 | }; 147 | 148 | if (geometry.Transform != null) 149 | serializer.Matrix = geometry.Transform.Value; 150 | 151 | return serializer; 152 | } 153 | 154 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 155 | { 156 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 157 | 158 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 159 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 160 | 161 | this.IsFinish = true; 162 | 163 | this.Draw(); 164 | } 165 | 166 | #endregion 167 | 168 | #region 字段 169 | 170 | private List points = new List(); 171 | private Point? mousePoint; 172 | 173 | #endregion 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /CurveDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | 8 | namespace DrawTools 9 | { 10 | /// 11 | /// 曲线 12 | /// 13 | public sealed class CurveDrawTool : DrawGeometryBase 14 | { 15 | public CurveDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 16 | { 17 | this.DrawingToolType = DrawToolType.Curve; 18 | 19 | // 准备要处理的事件 20 | this.CanTouchDown = true; 21 | } 22 | 23 | #region 鼠标键盘事件 24 | 25 | public override Boolean OnTouchLeave(Point point) 26 | { 27 | if (mousePoint.HasValue) 28 | { 29 | points.Add(mousePoint.Value); 30 | mousePoint = null; 31 | } 32 | 33 | if (points.Count < 2) 34 | this.drawingCanvas.DeleteVisual(this); 35 | else 36 | { 37 | geometry = geometry.GetWidenedPathGeometry(pen); 38 | Draw(); 39 | } 40 | 41 | this.drawingCanvas.DeleteWorkingDrawTool(this); 42 | 43 | this.IsFinish = true; 44 | 45 | this.CanTouchDown = false; 46 | this.CanTouchMove = false; 47 | this.CanTouchLeave = false; 48 | 49 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 50 | this.drawingCanvas.ReleaseMouseCapture(); 51 | 52 | return true; 53 | } 54 | 55 | public override Boolean OnTouchDown(Int32 touchId, Point point) 56 | { 57 | this.TouchId = touchId; 58 | 59 | if (points.Count == 0) 60 | { 61 | this.drawingCanvas.AddWorkingDrawTool(this); 62 | 63 | this.pen = this.drawingCanvas.Pen; 64 | 65 | points.Add(point); 66 | 67 | geometry = new PathGeometry(); 68 | 69 | var figure = new PathFigure { StartPoint = point }; 70 | pathGeometry.Figures.Add(figure); 71 | 72 | this.CanTouchMove = true; 73 | 74 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 75 | this.CanTouchLeave = true; 76 | 77 | this.drawingCanvas.AddVisual(this); 78 | } 79 | else if ((point - points.Last()).Length <= pen.Thickness) 80 | return OnTouchLeave(point); 81 | else if (mousePoint.HasValue) 82 | { 83 | points.Add(mousePoint.Value); 84 | mousePoint = null; 85 | } 86 | 87 | return true; 88 | } 89 | 90 | public override Boolean OnTouchMove(Point point) 91 | { 92 | var last = points.Last(); 93 | 94 | if ((point - last).Length <= pen.Thickness) 95 | return true; 96 | 97 | var figure = pathGeometry.Figures[0]; 98 | var centerX = (last.X + point.X) / 2; 99 | var bezier = new BezierSegment(new Point(centerX, last.Y), new Point(centerX, point.Y), point, true) { IsSmoothJoin = true }; 100 | 101 | if (mousePoint.HasValue) 102 | figure.Segments[figure.Segments.Count - 1] = bezier; 103 | else 104 | figure.Segments.Add(bezier); 105 | 106 | mousePoint = point; 107 | 108 | var dc = this.RenderOpen(); 109 | dc.DrawGeometry(null, pen, geometry); 110 | dc.Close(); 111 | 112 | return true; 113 | } 114 | 115 | #endregion 116 | 117 | #region 序列化 118 | 119 | public override DrawGeometrySerializerBase ToSerializer() 120 | { 121 | var serializer = new DrawCurveSerializer 122 | { 123 | Color = ((SolidColorBrush)pen.Brush).Color, 124 | StrokeThickness = pen.Thickness, 125 | Geometry = geometry.ToString() 126 | }; 127 | 128 | if (geometry.Transform != null) 129 | serializer.Matrix = geometry.Transform.Value; 130 | 131 | return serializer; 132 | } 133 | 134 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 135 | { 136 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 137 | 138 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 139 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 140 | 141 | this.IsFinish = true; 142 | 143 | this.Draw(); 144 | } 145 | 146 | #endregion 147 | 148 | #region 字段 149 | 150 | private List points = new List(); 151 | private Point? mousePoint; 152 | 153 | #endregion 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /DrawCursors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32.SafeHandles; 2 | using System; 3 | using System.Drawing; 4 | using System.Drawing.Drawing2D; 5 | using System.Runtime.InteropServices; 6 | using System.Security.Permissions; 7 | using System.Windows; 8 | using System.Windows.Input; 9 | using System.Windows.Interop; 10 | 11 | namespace DrawTools 12 | { 13 | public static class DrawCursors 14 | { 15 | public static Cursor CreateBmpCursor(Bitmap bmp, UInt32 xHotSpot, UInt32 yHotSpot) 16 | { 17 | if (!NativeMethods.GetIconInfo(bmp.GetHicon(), out IconInfo iconInfo)) 18 | return Cursors.Arrow; 19 | 20 | iconInfo.xHotspot = xHotSpot; // 焦点x轴坐标 21 | iconInfo.yHotspot = yHotSpot; // 焦点y轴坐标 22 | iconInfo.fIcon = false; // 设置鼠标 23 | 24 | var cursorHandle = NativeMethods.CreateIconIndirect(ref iconInfo); 25 | return CursorInteropHelper.Create(cursorHandle); 26 | } 27 | 28 | public static Cursor CreateBmpCursor(Uri uri, UInt32 xHotSpot, UInt32 yHotSpot) 29 | { 30 | var sri = Application.GetResourceStream(uri); 31 | 32 | using (var bmp = new Bitmap(sri.Stream)) 33 | { 34 | return CreateBmpCursor(bmp, xHotSpot, yHotSpot); 35 | } 36 | } 37 | 38 | public static Cursor CreateBmpCursor(UInt32 width, UInt32 height, UInt32 border, System.Windows.Media.SolidColorBrush brush) 39 | { 40 | using (var bmp = new Bitmap((Int32)(width + border * 2), (Int32)(height + border * 2))) 41 | { 42 | using (var g = Graphics.FromImage(bmp)) 43 | { 44 | g.SmoothingMode = SmoothingMode.HighQuality; 45 | g.InterpolationMode = InterpolationMode.HighQualityBicubic; 46 | 47 | var color = brush.Color; 48 | using (var pen = new Pen(Color.FromArgb(color.A, color.R, color.G, color.B), border)) 49 | { 50 | g.DrawRectangle(pen, border, border, width, height); 51 | g.Flush(); 52 | } 53 | } 54 | 55 | return CreateBmpCursor(bmp, width / 2 + border, height / 2 + border); 56 | } 57 | } 58 | 59 | private static Lazy hand = new Lazy(() => { return CreateBmpCursor(new Uri("Images/Cursor/hand.png", UriKind.Relative), 12, 12); }); 60 | /// 61 | /// 拖动 62 | /// 63 | public static Cursor Hand => hand.Value; 64 | } 65 | 66 | public static class NativeMethods 67 | { 68 | [DllImport("user32.dll")] 69 | public static extern SafeIconHandle CreateIconIndirect(ref IconInfo icon); 70 | 71 | [DllImport("user32.dll")] 72 | public static extern Boolean DestroyIcon(IntPtr hIcon); 73 | 74 | [DllImport("user32.dll")] 75 | [return: MarshalAs(UnmanagedType.Bool)] 76 | public static extern Boolean GetIconInfo(IntPtr hIcon, out IconInfo pIconInfo); 77 | } 78 | 79 | public struct IconInfo 80 | { 81 | public Boolean fIcon; 82 | public UInt32 xHotspot; 83 | public UInt32 yHotspot; 84 | public IntPtr hbmMask; 85 | public IntPtr hbmColor; 86 | } 87 | 88 | [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] 89 | public class SafeIconHandle : SafeHandleZeroOrMinusOneIsInvalid 90 | { 91 | public SafeIconHandle() : base(true) { } 92 | protected override Boolean ReleaseHandle() => NativeMethods.DestroyIcon(handle); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /DrawGeometryBase.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools 8 | { 9 | /// 10 | /// 画图几何图形基类 11 | /// 12 | public abstract class DrawGeometryBase : DrawingVisual, IDrawTool 13 | { 14 | public DrawGeometryBase(DrawingCanvas drawingCanvas) 15 | { 16 | this.drawingCanvas = drawingCanvas; 17 | } 18 | 19 | #region 鼠标键盘事件 20 | 21 | public virtual Boolean OnKeyDown(Key key) => false; 22 | 23 | public virtual Boolean OnKeyUp(Key key) => false; 24 | 25 | public virtual Boolean OnTouchDown(Int32 touchId, Point point) => false; 26 | 27 | public virtual Boolean OnTouchEnter(Point point) => false; 28 | 29 | public virtual Boolean OnTouchLeave(Point point) => false; 30 | 31 | public virtual Boolean OnTouchMove(Point point) => false; 32 | 33 | public virtual Boolean OnTouchUp(Point point) => false; 34 | 35 | #endregion 36 | 37 | #region 绘图事件 38 | 39 | public virtual void Draw() 40 | { 41 | var dc = this.RenderOpen(); 42 | dc.DrawGeometry(pen.Brush, null, geometry); 43 | dc.Close(); 44 | } 45 | 46 | public virtual Boolean Erase(Geometry erase) 47 | { 48 | geometry = Geometry.Combine(geometry, erase, GeometryCombineMode.Exclude, null); 49 | 50 | if (geometry.IsEmpty()) 51 | return true; 52 | 53 | Draw(); 54 | 55 | return false; 56 | } 57 | 58 | public virtual Boolean Select(Point point) 59 | { 60 | return geometry.FillContains(point); 61 | } 62 | 63 | public virtual Boolean Select(Geometry select) 64 | { 65 | return !Geometry.Combine(geometry, select, GeometryCombineMode.Intersect, null).IsEmpty(); 66 | } 67 | 68 | public virtual Rect Selected() 69 | { 70 | if (Mode == 1) 71 | return selectRect; 72 | 73 | Mode = 1; 74 | 75 | var dc = this.RenderOpen(); 76 | 77 | dc.DrawGeometry(pen.Brush, null, geometry); 78 | 79 | selectRect = GetRenderBounds(); 80 | 81 | dc.DrawRectangle(Brushes.Transparent, this.drawingCanvas.SelectBackgroundPen, selectRect); 82 | dc.DrawRectangle(null, this.drawingCanvas.SelectPen, selectRect); 83 | 84 | dc.Close(); 85 | 86 | return selectRect; 87 | } 88 | 89 | public virtual void Unselected() 90 | { 91 | if (Mode == 0) 92 | return; 93 | 94 | Mode = 0; 95 | 96 | Draw(); 97 | } 98 | 99 | protected virtual Rect GetRenderBounds() 100 | { 101 | return geometry.GetRenderBounds(this.drawingCanvas.SelectPen); 102 | } 103 | 104 | public virtual void Move(Double dx, Double dy) 105 | { 106 | if (geometry.Transform == null) 107 | geometry.Transform = new TranslateTransform(dx, dy); 108 | else 109 | { 110 | var translate = (TranslateTransform)geometry.Transform; 111 | translate.X += dx; 112 | translate.Y += dy; 113 | } 114 | 115 | if (Mode == 1) 116 | { 117 | Mode = 0; 118 | Selected(); 119 | } 120 | else 121 | Draw(); 122 | } 123 | 124 | public virtual void Edit() { } 125 | 126 | #endregion 127 | 128 | #region 序列化 129 | 130 | public virtual DrawGeometrySerializerBase ToSerializer() => null; 131 | 132 | public virtual void DeserializeFrom(DrawGeometrySerializerBase serializer) { } 133 | 134 | #endregion 135 | 136 | #region 属性 137 | 138 | public Int32 TouchId { get; protected set; } 139 | 140 | public Boolean CanTouchEnter { get; protected set; } 141 | 142 | public Boolean CanTouchLeave { get; protected set; } 143 | 144 | public Boolean CanTouchDown { get; protected set; } 145 | 146 | public Boolean CanTouchMove { get; protected set; } 147 | 148 | public Boolean CanTouchUp { get; protected set; } 149 | 150 | public Boolean CanKeyDown { get; protected set; } 151 | 152 | public Boolean CanKeyUp { get; protected set; } 153 | 154 | public Boolean IsFinish { get; protected set; } 155 | 156 | public DrawToolType DrawingToolType { get; protected set; } 157 | 158 | public Boolean CanEdit { get; protected set; } 159 | 160 | public Int32 Mode { get; protected set; } 161 | 162 | #endregion 163 | 164 | #region 字段 165 | 166 | protected DrawingCanvas drawingCanvas; 167 | protected Geometry geometry; 168 | protected PathGeometry pathGeometry => (PathGeometry)geometry; 169 | protected Pen pen; 170 | protected Rect selectRect; 171 | 172 | #endregion 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /DrawToolType.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools 2 | { 3 | /// 4 | /// 画图工具类型 5 | /// 6 | public enum DrawToolType 7 | { 8 | /// 9 | /// 指针(拾取) 10 | /// 11 | Pointer, 12 | /// 13 | /// 画笔 14 | /// 15 | Pen, 16 | /// 17 | /// 橡皮擦 18 | /// 19 | Eraser, 20 | /// 21 | /// 测距 22 | /// 23 | Ranging, 24 | /// 25 | /// 直线 26 | /// 27 | Line, 28 | /// 29 | /// 箭头 30 | /// 31 | Arrow, 32 | /// 33 | /// 矩形 34 | /// 35 | Rectangle, 36 | /// 37 | /// 椭圆 38 | /// 39 | Ellipse, 40 | /// 41 | /// 角度 42 | /// 43 | Angle, 44 | /// 45 | /// 折线 46 | /// 47 | Polyline, 48 | /// 49 | /// 曲线 50 | /// 51 | Curve, 52 | /// 53 | /// 多边形 54 | /// 55 | Polygon, 56 | /// 57 | /// 闭合曲线 58 | /// 59 | ClosedCurve, 60 | /// 61 | /// 面积 62 | /// 63 | Area, 64 | /// 65 | /// 文字 66 | /// 67 | Text 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /DrawTools.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {BE0F5277-A9A5-4235-AD1D-A68A72EDA03C} 8 | WinExe 9 | DrawTools 10 | DrawTools 11 | v4.5.2 12 | 512 13 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 14 | 4 15 | true 16 | true 17 | 18 | 19 | AnyCPU 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | AnyCPU 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | packages\HandyControl.3.0.0\lib\net45\HandyControl.dll 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 4.0 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | MSBuild:Compile 63 | Designer 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 | Designer 98 | MSBuild:Compile 99 | 100 | 101 | MSBuild:Compile 102 | Designer 103 | 104 | 105 | App.xaml 106 | Code 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | MainWindow.xaml 117 | Code 118 | 119 | 120 | 121 | 122 | 123 | 124 | Code 125 | 126 | 127 | True 128 | True 129 | Resources.resx 130 | 131 | 132 | True 133 | Settings.settings 134 | True 135 | 136 | 137 | ResXFileCodeGenerator 138 | Resources.Designer.cs 139 | 140 | 141 | 142 | SettingsSingleFileGenerator 143 | Settings.Designer.cs 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 | -------------------------------------------------------------------------------- /DrawTools.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30907.101 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawTools", "DrawTools.csproj", "{BE0F5277-A9A5-4235-AD1D-A68A72EDA03C}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {BE0F5277-A9A5-4235-AD1D-A68A72EDA03C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {BE0F5277-A9A5-4235-AD1D-A68A72EDA03C}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {BE0F5277-A9A5-4235-AD1D-A68A72EDA03C}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {BE0F5277-A9A5-4235-AD1D-A68A72EDA03C}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {F7D098F6-7E6C-4092-9225-34682866B227} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /DrawingCanvas.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using DrawTools.Utils; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Markup; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Xml.Serialization; 15 | 16 | namespace DrawTools 17 | { 18 | [ContentProperty] 19 | public sealed class DrawingCanvas : Canvas 20 | { 21 | public DrawingCanvas() 22 | { 23 | this.ClipToBounds = true; 24 | this.Focusable = true; 25 | this.OriginalCursor = Cursors.Arrow; 26 | this.squareCursor = false; 27 | 28 | this.SelectBackgroundPen = new Pen(Brushes.White, 1); 29 | this.SelectPen = new Pen(Brushes.Black, 1) { DashStyle = new DashStyle(new Double[] { 4 }, 0) }; 30 | 31 | this.Loaded += delegate 32 | { 33 | this.Focus(); 34 | this.drawViewer = this.FindParent(); 35 | }; 36 | } 37 | 38 | #region 可视化 39 | 40 | private List visuals = new List(); 41 | 42 | protected override Int32 VisualChildrenCount => visuals.Count + Children.Count; 43 | 44 | protected override Visual GetVisualChild(Int32 index) 45 | { 46 | if (index < visuals.Count) 47 | return visuals[index]; 48 | else 49 | return Children[index - visuals.Count]; 50 | } 51 | 52 | public void AddVisual(Visual visual) 53 | { 54 | visuals.Add(visual); 55 | 56 | base.AddVisualChild(visual); 57 | base.AddLogicalChild(visual); 58 | } 59 | 60 | public void Insert(Int32 index, Visual visual) 61 | { 62 | visuals.Insert(index, visual); 63 | 64 | base.AddVisualChild(visual); 65 | base.AddLogicalChild(visual); 66 | } 67 | 68 | public void MovoToHead(Visual visual) 69 | { 70 | var index = visuals.IndexOf(visual); 71 | 72 | if (index <= 0) 73 | return; 74 | 75 | visuals.RemoveAt(index); 76 | visuals.Insert(0, visual); 77 | } 78 | 79 | public void DeleteVisual(Visual visual) 80 | { 81 | visuals.Remove(visual); 82 | 83 | base.RemoveVisualChild(visual); 84 | base.RemoveLogicalChild(visual); 85 | } 86 | 87 | public DrawingVisual GetVisual(Point point) 88 | { 89 | var hitResult = VisualTreeHelper.HitTest(this, point); 90 | return hitResult.VisualHit as DrawingVisual; 91 | } 92 | 93 | #endregion 94 | 95 | #region 依赖属性 96 | 97 | public static readonly DependencyProperty BrushProperty = DependencyProperty.Register("Brush", typeof(SolidColorBrush), typeof(DrawingCanvas), new PropertyMetadata(Brushes.Black, OnBrushPropertyChanged)); 98 | 99 | private static void OnBrushPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 100 | { 101 | ((DrawingCanvas)d).UpdateCursor(); 102 | } 103 | 104 | /// 105 | /// 画刷颜色 106 | /// 107 | public SolidColorBrush Brush { get => (SolidColorBrush)this.GetValue(BrushProperty); set => this.SetValue(BrushProperty, value); } 108 | 109 | public static readonly DependencyProperty StrokeThicknessProperty = DependencyProperty.Register("StrokeThickness", typeof(UInt32), typeof(DrawingCanvas), new PropertyMetadata(1u, OnStrokeThicknessPropertyChanged)); 110 | 111 | private static void OnStrokeThicknessPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 112 | { 113 | ((DrawingCanvas)d).UpdateCursor(); 114 | } 115 | 116 | /// 117 | /// 画刷宽度 118 | /// 119 | public UInt32 StrokeThickness { get => (UInt32)this.GetValue(StrokeThicknessProperty); set => this.SetValue(StrokeThicknessProperty, value); } 120 | 121 | public static readonly DependencyProperty DrawingToolTypeProperty = DependencyProperty.Register("DrawingToolType", typeof(DrawToolType), typeof(DrawingCanvas), new PropertyMetadata(DrawToolType.Pointer, OnDrawingToolTypePropertyChanged)); 122 | 123 | private static void OnDrawingToolTypePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 124 | { 125 | var drawing = (DrawingCanvas)d; 126 | 127 | if (!drawing.IsKeyboardFocused) 128 | drawing.Focus(); 129 | 130 | drawing.UpdateCursor(); 131 | } 132 | 133 | /// 134 | /// 当前的画图工具 135 | /// 136 | public DrawToolType DrawingToolType { get => (DrawToolType)this.GetValue(DrawingToolTypeProperty); set => this.SetValue(DrawingToolTypeProperty, value); } 137 | 138 | public static readonly DependencyProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(typeof(DrawingCanvas), new FrameworkPropertyMetadata(12d, FrameworkPropertyMetadataOptions.Inherits)); 139 | /// 140 | /// 文本大小 141 | /// 142 | public Double FontSize { get => (Double)this.GetValue(FontSizeProperty); set => this.SetValue(FontSizeProperty, value); } 143 | 144 | public static readonly DependencyProperty ZoomProperty = DrawingCanvasViewer.ZoomProperty.AddOwner(typeof(DrawingCanvas), new FrameworkPropertyMetadata(1d, FrameworkPropertyMetadataOptions.Inherits, new PropertyChangedCallback(OnZoomPropertyChanged))); 145 | 146 | private static void OnZoomPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 147 | { 148 | if (!Double.IsNaN((Double)e.NewValue)) 149 | ((DrawingCanvas)d).UpdateCursor(); 150 | } 151 | 152 | /// 153 | /// X轴缩放 154 | /// 155 | public Double Zoom { get => (Double)this.GetValue(ZoomProperty); set => this.SetValue(ZoomProperty, value); } 156 | 157 | #endregion 158 | 159 | #region 鼠标键盘事件 160 | 161 | protected override void OnMouseEnter(MouseEventArgs e) 162 | { 163 | IDrawTool tool; 164 | 165 | var point = e.GetPosition(this); 166 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 167 | { 168 | tool = workingDrawTools[i]; 169 | 170 | if (tool.TouchId == 0 && tool.CanTouchEnter && (e.Handled = tool.OnTouchEnter(point))) 171 | return; 172 | } 173 | } 174 | 175 | protected override void OnMouseLeave(MouseEventArgs e) 176 | { 177 | lastPoint = null; 178 | 179 | IDrawTool tool; 180 | 181 | var point = e.GetPosition(this); 182 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 183 | { 184 | tool = workingDrawTools[i]; 185 | 186 | if (tool.TouchId == 0 && tool.CanTouchLeave && (e.Handled = tool.OnTouchLeave(point))) 187 | return; 188 | } 189 | } 190 | 191 | protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) 192 | { 193 | if (!this.IsKeyboardFocused) 194 | this.Focus(); 195 | 196 | if (this.canDragStart) 197 | { 198 | lastPoint = e.GetPosition(this.drawViewer); 199 | e.Handled = true; 200 | return; 201 | } 202 | 203 | IDrawTool tool; 204 | 205 | var point = e.GetPosition(this); 206 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 207 | { 208 | tool = workingDrawTools[i]; 209 | 210 | if (tool.CanTouchDown && (e.Handled = tool.OnTouchDown(0, point))) 211 | return; 212 | } 213 | 214 | tool = CreateDrawingTool(); 215 | 216 | if (tool.CanTouchDown && (e.Handled = tool.OnTouchDown(0, point))) 217 | return; 218 | 219 | if (this.canDragStart) 220 | { 221 | lastPoint = e.GetPosition(this); 222 | e.Handled = true; 223 | return; 224 | } 225 | } 226 | 227 | protected override void OnMouseMove(MouseEventArgs e) 228 | { 229 | if (this.canDragMove) 230 | { 231 | var drawPoint = e.GetPosition(this.drawViewer); 232 | var dx = lastPoint.Value.X - drawPoint.X; 233 | var dy = lastPoint.Value.Y - drawPoint.Y; 234 | this.drawViewer.ScrollBy(dx, dy); 235 | lastPoint = drawPoint; 236 | e.Handled = true; 237 | return; 238 | } 239 | 240 | var point = e.GetPosition(this); 241 | 242 | if (point.X < 0 || point.Y < 0 || point.X > this.ActualWidth || point.Y > this.ActualHeight) 243 | return; 244 | 245 | IDrawTool tool; 246 | 247 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 248 | { 249 | tool = workingDrawTools[i]; 250 | 251 | if (tool.TouchId == 0 && tool.CanTouchMove && (e.Handled = tool.OnTouchMove(e.GetPosition(this)))) 252 | return; 253 | } 254 | } 255 | 256 | protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e) 257 | { 258 | lastPoint = null; 259 | 260 | IDrawTool tool; 261 | 262 | var point = e.GetPosition(this); 263 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 264 | { 265 | tool = workingDrawTools[i]; 266 | 267 | if (tool.TouchId == 0 && tool.CanTouchUp && (e.Handled = tool.OnTouchUp(point))) 268 | return; 269 | } 270 | } 271 | 272 | protected override void OnKeyDown(KeyEventArgs e) 273 | { 274 | IDrawTool tool; 275 | 276 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 277 | { 278 | tool = workingDrawTools[i]; 279 | 280 | if (tool.CanKeyDown && (e.Handled = tool.OnKeyDown(e.Key))) 281 | return; 282 | } 283 | 284 | if (e.Key == Key.Space) 285 | { 286 | this.OriginalCursor = DrawCursors.Hand; 287 | this.squareCursor = false; 288 | } 289 | } 290 | 291 | protected override void OnKeyUp(KeyEventArgs e) 292 | { 293 | IDrawTool tool; 294 | 295 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 296 | { 297 | tool = workingDrawTools[i]; 298 | 299 | if (tool.CanKeyUp && (e.Handled = tool.OnKeyUp(e.Key))) 300 | return; 301 | } 302 | 303 | if (e.Key == Key.Space) 304 | this.UpdateCursor(); 305 | } 306 | 307 | #endregion 308 | 309 | #region 触摸事件 310 | 311 | protected override void OnTouchEnter(TouchEventArgs e) 312 | { 313 | IDrawTool tool; 314 | 315 | var point = e.GetTouchPoint(this).Position; 316 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 317 | { 318 | tool = workingDrawTools[i]; 319 | 320 | if (tool.TouchId == e.TouchDevice.Id && tool.CanTouchEnter && (e.Handled = tool.OnTouchEnter(point))) 321 | return; 322 | } 323 | } 324 | 325 | protected override void OnTouchLeave(TouchEventArgs e) 326 | { 327 | lastPoint = null; 328 | 329 | IDrawTool tool; 330 | 331 | var point = e.GetTouchPoint(this).Position; 332 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 333 | { 334 | tool = workingDrawTools[i]; 335 | 336 | if (tool.TouchId == e.TouchDevice.Id && tool.CanTouchLeave && (e.Handled = tool.OnTouchLeave(point))) 337 | return; 338 | } 339 | } 340 | 341 | protected override void OnTouchDown(TouchEventArgs e) 342 | { 343 | if (!this.IsKeyboardFocused) 344 | this.Focus(); 345 | 346 | if (this.canDragStart) 347 | { 348 | lastPoint = e.GetTouchPoint(this.drawViewer).Position; 349 | e.Handled = true; 350 | return; 351 | } 352 | 353 | IDrawTool tool; 354 | 355 | var point = e.GetTouchPoint(this).Position; 356 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 357 | { 358 | tool = workingDrawTools[i]; 359 | 360 | if (tool.CanTouchDown && (e.Handled = tool.OnTouchDown(0, point))) 361 | return; 362 | } 363 | 364 | tool = CreateDrawingTool(); 365 | 366 | if (tool.CanTouchDown && (e.Handled = tool.OnTouchDown(0, point))) 367 | return; 368 | 369 | if (this.canDragStart) 370 | { 371 | lastPoint = point; 372 | e.Handled = true; 373 | return; 374 | } 375 | } 376 | 377 | protected override void OnTouchMove(TouchEventArgs e) 378 | { 379 | if (this.canDragMove) 380 | { 381 | var drawPoint = e.GetTouchPoint(this.drawViewer).Position; 382 | var dx = lastPoint.Value.X - drawPoint.X; 383 | var dy = lastPoint.Value.Y - drawPoint.Y; 384 | this.drawViewer.ScrollBy(dx, dy); 385 | lastPoint = drawPoint; 386 | e.Handled = true; 387 | return; 388 | } 389 | 390 | var point = e.GetTouchPoint(this).Position; 391 | 392 | if (point.X < 0 || point.Y < 0 || point.X > this.ActualWidth || point.Y > this.ActualHeight) 393 | return; 394 | 395 | IDrawTool tool; 396 | 397 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 398 | { 399 | tool = workingDrawTools[i]; 400 | 401 | if (tool.TouchId == 0 && tool.CanTouchMove && (e.Handled = tool.OnTouchMove(point))) 402 | return; 403 | } 404 | } 405 | 406 | protected override void OnTouchUp(TouchEventArgs e) 407 | { 408 | lastPoint = null; 409 | 410 | IDrawTool tool; 411 | 412 | var point = e.GetTouchPoint(this).Position; 413 | for (var i = workingDrawTools.Count - 1; i >= 0; i--) 414 | { 415 | tool = workingDrawTools[i]; 416 | 417 | if (tool.TouchId == 0 && tool.CanTouchUp && (e.Handled = tool.OnTouchUp(point))) 418 | return; 419 | } 420 | } 421 | 422 | #endregion 423 | 424 | #region 公开方法 425 | 426 | public void Clear() 427 | { 428 | foreach (var visual in visuals) 429 | { 430 | base.RemoveLogicalChild(visual); 431 | base.RemoveVisualChild(visual); 432 | } 433 | 434 | this.visuals.Clear(); 435 | this.Children.Clear(); 436 | this.workingDrawTools.Clear(); 437 | } 438 | 439 | public IEnumerable GetDrawGeometries() 440 | { 441 | return visuals.OfType().Where(q => q.IsFinish); 442 | } 443 | 444 | public void AddWorkingDrawTool(IDrawTool drawTool) 445 | { 446 | this.workingDrawTools.Add(drawTool); 447 | } 448 | 449 | public void DeleteWorkingDrawTool(IDrawTool drawTool) 450 | { 451 | this.workingDrawTools.Remove(drawTool); 452 | } 453 | 454 | /// 455 | /// 转为图片 456 | /// 457 | /// 458 | /// 459 | /// 460 | /// 461 | /// 462 | public BitmapFrame ToBitmapFrame(Int32 pixelWidth, Int32 pixelHeight, Dpi dpi, ImageSource image = null) 463 | { 464 | var visual = GetDrawingVisual(pixelWidth, pixelHeight, dpi, image); 465 | 466 | if (visual == null) 467 | return null; 468 | 469 | var renderBitmap = new RenderTargetBitmap(pixelWidth, pixelHeight, dpi.DpiX, dpi.DpiY, PixelFormats.Pbgra32); 470 | renderBitmap.Render(visual); 471 | 472 | return BitmapFrame.Create(renderBitmap); 473 | } 474 | 475 | /// 476 | /// 打印 477 | /// 478 | /// 479 | /// 480 | /// 481 | /// 482 | public void Print(Int32 pixelWidth, Int32 pixelHeight, Dpi dpi, ImageSource image = null) 483 | { 484 | var visual = GetDrawingVisual(pixelWidth, pixelHeight, dpi, image); 485 | 486 | if (visual == null) 487 | return; 488 | 489 | var printDlg = new PrintDialog(); 490 | 491 | if ((Boolean)printDlg.ShowDialog()) 492 | { 493 | var l = Math.Max(pixelWidth, pixelHeight); 494 | var s = Math.Min(pixelWidth, pixelHeight); 495 | 496 | var sizeL = Math.Max(printDlg.PrintableAreaWidth, printDlg.PrintableAreaHeight); 497 | var sizeS = Math.Min(printDlg.PrintableAreaWidth, printDlg.PrintableAreaHeight); 498 | 499 | var zoom = Math.Min(sizeL / l, sizeS / s); 500 | visual.Transform = new ScaleTransform(zoom, zoom); 501 | 502 | if (pixelWidth != pixelHeight && (pixelWidth > pixelHeight ^ printDlg.PrintableAreaWidth > printDlg.PrintableAreaHeight)) 503 | printDlg.PrintTicket.PageOrientation = System.Printing.PageOrientation.Landscape; 504 | 505 | printDlg.PrintVisual(visual, nameof(DrawingCanvas)); 506 | } 507 | } 508 | 509 | public void Save(String filepath) 510 | { 511 | var serializer = new DrawGeometrySerializer 512 | { 513 | Geometries = this.GetDrawGeometries().Select(q => q.ToSerializer()).ToArray() 514 | }; 515 | 516 | var xml = new XmlSerializer(typeof(DrawGeometrySerializer)); 517 | 518 | using (var fs = new FileStream(filepath, FileMode.Create, FileAccess.Write, FileShare.None)) 519 | { 520 | xml.Serialize(fs, serializer); 521 | } 522 | } 523 | 524 | public void Load(String filepath) 525 | { 526 | var xml = new XmlSerializer(typeof(DrawGeometrySerializer)); 527 | 528 | using (var fs = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.Read)) 529 | { 530 | var serializer = (DrawGeometrySerializer)xml.Deserialize(fs); 531 | 532 | this.Clear(); 533 | 534 | foreach (var draw in serializer.Geometries) 535 | { 536 | this.AddVisual(draw.Deserialize(this)); 537 | } 538 | } 539 | } 540 | 541 | #endregion 542 | 543 | #region 私有方法 544 | 545 | private IDrawTool CreateDrawingTool() 546 | { 547 | switch (DrawingToolType) 548 | { 549 | case DrawToolType.Pointer: 550 | return new PointerDrawTool(this); 551 | case DrawToolType.Pen: 552 | return new PenDrawTool(this); 553 | case DrawToolType.Eraser: 554 | return new EraserDrawTool(this); 555 | case DrawToolType.Line: 556 | return new LineDrawTool(this); 557 | case DrawToolType.Arrow: 558 | return new ArrowDrawTool(this); 559 | case DrawToolType.Ranging: 560 | return new RangingDrawTool(this); 561 | case DrawToolType.Rectangle: 562 | return new RectangleDrawTool(this); 563 | case DrawToolType.Ellipse: 564 | return new EllipseDrawTool(this); 565 | case DrawToolType.Angle: 566 | return new AngleDrawTool(this); 567 | case DrawToolType.Polyline: 568 | return new PolylineDrawTool(this); 569 | case DrawToolType.Curve: 570 | return new CurveDrawTool(this); 571 | case DrawToolType.Polygon: 572 | return new PolygonDrawTool(this); 573 | case DrawToolType.ClosedCurve: 574 | return new ClosedCurveDrawTool(this); 575 | case DrawToolType.Area: 576 | return new AreaDrawTool(this); 577 | case DrawToolType.Text: 578 | return new TextDrawTool(this); 579 | default: 580 | throw new ArgumentOutOfRangeException("不支持画图工具" + DrawingToolType); 581 | } 582 | } 583 | 584 | private void UpdateCursor() 585 | { 586 | switch (this.DrawingToolType) 587 | { 588 | case DrawToolType.Pointer: 589 | this.OriginalCursor = Cursors.Arrow; 590 | this.squareCursor = false; 591 | break; 592 | case DrawToolType.Text: 593 | this.OriginalCursor = Cursors.IBeam; 594 | this.squareCursor = false; 595 | break; 596 | default: 597 | if ((squareCursor && cursorLength == StrokeThickness && cursorZoom == Zoom) || Double.IsNaN(StrokeThickness) || Double.IsNaN(Zoom)) 598 | break; 599 | var w = (UInt32)Math.Max(1, StrokeThickness * Zoom); 600 | var h = (UInt32)Math.Max(1, StrokeThickness * Zoom); 601 | var border = (UInt32)Math.Max(1, 2 * Math.Min(1, Zoom)); 602 | this.OriginalCursor = DrawCursors.CreateBmpCursor(w, h, border, Brush); 603 | this.squareCursor = true; 604 | this.cursorLength = StrokeThickness; 605 | this.cursorZoom = Zoom; 606 | break; 607 | } 608 | } 609 | 610 | private DrawingVisual GetDrawingVisual(Int32 pixelWidth, Int32 pixelHeight, Dpi dpi, ImageSource image = null) 611 | { 612 | var drawGeometries = this.GetDrawGeometries(); 613 | 614 | if (drawGeometries.Count() == 0) 615 | return null; 616 | 617 | var root = new DrawingVisual(); 618 | 619 | var dc = root.RenderOpen(); 620 | 621 | if (image != null) 622 | dc.DrawImage(image, new Rect(new Size(pixelWidth * dpi.Px2WpfX, pixelHeight * dpi.Px2WpfY))); 623 | 624 | foreach (var draw in drawGeometries) 625 | { 626 | dc.DrawDrawing(draw.Drawing); 627 | } 628 | 629 | dc.Close(); 630 | 631 | return root; 632 | } 633 | 634 | #endregion 635 | 636 | #region 属性 637 | 638 | /// 639 | /// 画笔 640 | /// 641 | public Pen Pen => new Pen(this.Brush, this.StrokeThickness); 642 | /// 643 | /// 选择画笔 644 | /// 645 | public Pen SelectPen { get; } 646 | /// 647 | /// 选择底色画笔 648 | /// 649 | public Pen SelectBackgroundPen { get; } 650 | 651 | #endregion 652 | 653 | #region 字段 654 | 655 | /// 656 | /// 正在进行的画图工具 657 | /// 658 | private List workingDrawTools = new List(); 659 | 660 | private Cursor originalCursor; 661 | /// 662 | /// 原始指针 663 | /// 664 | public Cursor OriginalCursor 665 | { 666 | get => originalCursor; 667 | set 668 | { 669 | if (originalCursor == value) 670 | return; 671 | 672 | originalCursor = value; 673 | 674 | if (!handleCursor) 675 | this.Cursor = value; 676 | } 677 | } 678 | 679 | /// 680 | /// 鼠标指针是否正在被控制 681 | /// 682 | public Boolean handleCursor; 683 | 684 | public Boolean squareCursor; 685 | public UInt32 cursorLength; 686 | public Double cursorZoom; 687 | 688 | private Point? lastPoint; 689 | private DrawingCanvasViewer drawViewer; 690 | private Boolean canDragStart => this.Cursor == DrawCursors.Hand && drawViewer != null; 691 | private Boolean canDragMove => canDragStart && lastPoint.HasValue; 692 | 693 | #endregion 694 | } 695 | } 696 | -------------------------------------------------------------------------------- /DrawingCanvasViewer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using System.Windows.Markup; 6 | using System.Windows.Media.Imaging; 7 | 8 | namespace DrawTools 9 | { 10 | [ContentProperty("DrawingCanvas")] 11 | [TemplatePart(Name = "Part_ScrollViewer", Type = typeof(ScrollViewer))] 12 | public sealed class DrawingCanvasViewer : Control 13 | { 14 | #region 依赖属性 15 | 16 | public static readonly DependencyProperty DrawingCanvasProperty = DependencyProperty.Register("DrawingCanvas", typeof(DrawingCanvas), typeof(DrawingCanvasViewer)); 17 | /// 18 | /// 画板 19 | /// 20 | public DrawingCanvas DrawingCanvas { get => (DrawingCanvas)this.GetValue(DrawingCanvasProperty); set => this.SetValue(DrawingCanvasProperty, value); } 21 | 22 | public static readonly DependencyProperty BackgroundImageProperty = DependencyProperty.Register("BackgroundImage", typeof(BitmapSource), typeof(DrawingCanvasViewer), new PropertyMetadata(OnBackgroundImagePropertyChanged)); 23 | 24 | private static void OnBackgroundImagePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 25 | { 26 | var viewer = (DrawingCanvasViewer)d; 27 | viewer.EnsureZoom(); 28 | viewer.EnsureInnerSize(); 29 | } 30 | 31 | /// 32 | /// 背景图 33 | /// 34 | public BitmapSource BackgroundImage { get => (BitmapSource)this.GetValue(BackgroundImageProperty); set => this.SetValue(BackgroundImageProperty, value); } 35 | 36 | public static readonly DependencyProperty InnerWidthProperty = DependencyProperty.Register("InnerWidth", typeof(Double), typeof(DrawingCanvasViewer), new PropertyMetadata(Double.NaN)); 37 | 38 | public Double InnerWidth { get => (Double)this.GetValue(InnerWidthProperty); set => this.SetValue(InnerWidthProperty, value); } 39 | 40 | public static readonly DependencyProperty InnerHeightProperty = DependencyProperty.Register("InnerHeight", typeof(Double), typeof(DrawingCanvasViewer), new PropertyMetadata(Double.NaN)); 41 | 42 | public Double InnerHeight { get => (Double)this.GetValue(InnerHeightProperty); set => this.SetValue(InnerHeightProperty, value); } 43 | 44 | public static readonly DependencyProperty ImageWidthProperty = DependencyProperty.Register("ImageWidth", typeof(Double), typeof(DrawingCanvasViewer), new PropertyMetadata(Double.NaN)); 45 | 46 | public Double ImageWidth { get => (Double)this.GetValue(ImageWidthProperty); set => this.SetValue(ImageWidthProperty, value); } 47 | 48 | public static readonly DependencyProperty ImageHeightProperty = DependencyProperty.Register("ImageHeight", typeof(Double), typeof(DrawingCanvasViewer), new PropertyMetadata(Double.NaN)); 49 | 50 | public Double ImageHeight { get => (Double)this.GetValue(ImageHeightProperty); set => this.SetValue(ImageHeightProperty, value); } 51 | 52 | public static readonly DependencyProperty ZoomProperty = DependencyProperty.Register("Zoom", typeof(Double), typeof(DrawingCanvasViewer), new PropertyMetadata(Double.NaN, OnZoomPropertyChanged)); 53 | 54 | private static void OnZoomPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 55 | { 56 | var viewer = (DrawingCanvasViewer)d; 57 | viewer.EnsureInnerSize(); 58 | viewer.EnsureImageSize(); 59 | } 60 | 61 | /// 62 | /// 缩放百分比 63 | /// 64 | public Double Zoom { get => (Double)this.GetValue(ZoomProperty); set => this.SetValue(ZoomProperty, value); } 65 | 66 | #endregion 67 | 68 | #region 构造器 69 | 70 | static DrawingCanvasViewer() 71 | { 72 | DefaultStyleKeyProperty.OverrideMetadata(typeof(DrawingCanvasViewer), new FrameworkPropertyMetadata(typeof(DrawingCanvasViewer))); 73 | } 74 | 75 | public DrawingCanvasViewer() 76 | { 77 | this.Loaded += OnFirstLoaded; 78 | } 79 | 80 | #endregion 81 | 82 | #region 公开方法 83 | 84 | public void ScrollBy(Double dx, Double dy) 85 | { 86 | if (!CanDrag) 87 | return; 88 | 89 | if (dx != 0) 90 | scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + dx); 91 | if (dy != 0) 92 | scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + dy); 93 | } 94 | 95 | #endregion 96 | 97 | #region 私有方法 98 | 99 | public override void OnApplyTemplate() 100 | { 101 | base.OnApplyTemplate(); 102 | 103 | if (scrollViewer != null) 104 | scrollViewer.ScrollChanged -= OnScrollChanged; 105 | 106 | scrollViewer = this.Template.FindName("Part_ScrollViewer", this) as ScrollViewer; 107 | 108 | if (scrollViewer == null) 109 | throw new NullReferenceException("模板找不到Part_ScrollViewer"); 110 | } 111 | 112 | private void OnFirstLoaded(Object sender, RoutedEventArgs e) 113 | { 114 | this.Loaded -= OnFirstLoaded; 115 | 116 | if (BackgroundImage == null || scrollViewer == null) 117 | return; 118 | 119 | if (Double.IsNaN(this.Zoom)) 120 | EnsureZoom(); 121 | 122 | EnsureInnerSize(); 123 | 124 | this.SizeChanged += OnSizeChanged; 125 | OnSizeChanged(sender, e); 126 | 127 | scrollViewer.ScrollChanged += OnScrollChanged; 128 | } 129 | 130 | private void EnsureZoom() 131 | { 132 | if (BackgroundImage == null || scrollViewer == null) 133 | { 134 | this.ClearValue(ZoomProperty); 135 | return; 136 | } 137 | 138 | var zoom = Math.Min(scrollViewer.ViewportWidth / BackgroundImage.Width, scrollViewer.ViewportHeight / BackgroundImage.Height); 139 | this.SetCurrentValue(ZoomProperty, Math.Min(1, zoom)); 140 | } 141 | 142 | private void EnsureInnerSize() 143 | { 144 | if (BackgroundImage == null || scrollViewer == null || Double.IsNaN(this.Zoom)) 145 | { 146 | this.ClearValue(InnerWidthProperty); 147 | this.ClearValue(InnerHeightProperty); 148 | return; 149 | } 150 | 151 | var width = BackgroundImage.Width * this.Zoom; 152 | var height = BackgroundImage.Height * this.Zoom; 153 | 154 | if (width < scrollViewer.ViewportWidth) 155 | width = scrollViewer.ViewportWidth; 156 | 157 | if (height < scrollViewer.ViewportHeight) 158 | height = scrollViewer.ViewportHeight; 159 | 160 | scrollViewer.ScrollChanged -= OnScrollChanged; 161 | 162 | this.SetValue(InnerWidthProperty, width); 163 | this.SetValue(InnerHeightProperty, height); 164 | 165 | scrollViewer.ScrollChanged += OnScrollChanged; 166 | 167 | this.EnsureScrollOffset(); 168 | } 169 | 170 | private void EnsureImageSize() 171 | { 172 | if (BackgroundImage == null || Double.IsNaN(this.Zoom)) 173 | { 174 | this.ClearValue(ImageWidthProperty); 175 | this.ClearValue(ImageHeightProperty); 176 | return; 177 | } 178 | 179 | this.SetValue(ImageWidthProperty, BackgroundImage.Width * this.Zoom); 180 | this.SetValue(ImageHeightProperty, BackgroundImage.Height * this.Zoom); 181 | } 182 | 183 | private void EnsureScrollOffset() 184 | { 185 | var offsetX = (this.InnerWidth - scrollViewer.ActualWidth) * center.X; 186 | var offsetY = (this.InnerHeight - scrollViewer.ActualHeight) * center.Y; 187 | 188 | scrollViewer.ScrollToHorizontalOffset(offsetX); 189 | scrollViewer.ScrollToVerticalOffset(offsetY); 190 | } 191 | 192 | private void OnSizeChanged(Object sender, RoutedEventArgs e) 193 | { 194 | this.EnsureScrollOffset(); 195 | } 196 | 197 | private void OnScrollChanged(Object sender, ScrollChangedEventArgs e) 198 | { 199 | if (this.InnerWidth == scrollViewer.ActualWidth) 200 | { 201 | center.X = 0.5; 202 | center.Y = 0.5; 203 | } 204 | else 205 | { 206 | center.X = (this.InnerWidth == scrollViewer.ActualWidth) ? 0 : scrollViewer.HorizontalOffset / (this.InnerWidth - scrollViewer.ActualWidth); 207 | center.Y = (this.InnerHeight == scrollViewer.ActualHeight) ? 0 : scrollViewer.VerticalOffset / (this.InnerHeight - scrollViewer.ActualHeight); 208 | } 209 | } 210 | 211 | protected override void OnPreviewMouseWheel(MouseWheelEventArgs e) 212 | { 213 | if (Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) 214 | { 215 | Zoom *= 1 + (e.Delta > 0 ? scale : -scale); 216 | e.Handled = true; 217 | } 218 | } 219 | 220 | #endregion 221 | 222 | #region 属性 223 | 224 | public Boolean CanDrag => this.scrollViewer != null && (InnerHeight > scrollViewer.ViewportHeight || InnerWidth > scrollViewer.ViewportWidth); 225 | 226 | #endregion 227 | 228 | #region 字段 229 | 230 | private ScrollViewer scrollViewer; 231 | private Point center = new Point(0.5, 0.5); 232 | private Double scale = 0.05; 233 | 234 | #endregion 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /EllipseDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools 8 | { 9 | /// 10 | /// 椭圆 11 | /// 12 | public sealed class EllipseDrawTool : DrawGeometryBase 13 | { 14 | public EllipseDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 15 | { 16 | this.DrawingToolType = DrawToolType.Ellipse; 17 | 18 | // 准备要处理的事件 19 | this.CanTouchDown = true; 20 | } 21 | 22 | #region 鼠标键盘事件 23 | 24 | public override Boolean OnKeyDown(Key key) 25 | { 26 | if (key != Key.LeftShift || !bottomRight.HasValue) 27 | return false; 28 | 29 | return OnTouchMove(bottomRight.Value); 30 | } 31 | 32 | public override Boolean OnKeyUp(Key key) 33 | { 34 | if (key != Key.LeftShift || !bottomRight.HasValue) 35 | return false; 36 | 37 | return OnTouchMove(bottomRight.Value); 38 | } 39 | 40 | public override Boolean OnTouchLeave(Point point) 41 | { 42 | if (!bottomRight.HasValue) 43 | this.drawingCanvas.DeleteVisual(this); 44 | else 45 | { 46 | var figure = pathGeometry.Figures[0]; 47 | figure.StartPoint = new Point(topLeft.Value.X, center.Y); 48 | 49 | var clockwise = bottomRight.Value.X > topLeft.Value.X ? SweepDirection.Clockwise : SweepDirection.Counterclockwise; 50 | var arc = new ArcSegment(new Point(bottomRight.Value.X, center.Y), new Size(radiusX, radiusY), 0, false, clockwise, true); 51 | figure.Segments.Add(arc); 52 | 53 | arc = new ArcSegment(figure.StartPoint, new Size(radiusX, radiusY), 0, false, clockwise, true); 54 | figure.Segments.Add(arc); 55 | 56 | geometry = geometry.GetWidenedPathGeometry(pen); 57 | Draw(); 58 | } 59 | 60 | this.drawingCanvas.DeleteWorkingDrawTool(this); 61 | 62 | this.IsFinish = true; 63 | 64 | this.CanTouchDown = false; 65 | this.CanTouchMove = false; 66 | this.CanTouchLeave = false; 67 | this.CanKeyDown = false; 68 | this.CanKeyUp = false; 69 | 70 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 71 | this.drawingCanvas.ReleaseMouseCapture(); 72 | 73 | return true; 74 | } 75 | 76 | public override Boolean OnTouchDown(Int32 touchId, Point point) 77 | { 78 | this.TouchId = touchId; 79 | 80 | if (!topLeft.HasValue) 81 | { 82 | this.drawingCanvas.AddWorkingDrawTool(this); 83 | 84 | this.pen = this.drawingCanvas.Pen; 85 | 86 | topLeft = point; 87 | 88 | this.geometry = new PathGeometry(); 89 | 90 | var figure = new PathFigure { IsClosed = true }; 91 | pathGeometry.Figures.Add(figure); 92 | 93 | this.CanTouchMove = true; 94 | this.CanKeyDown = true; 95 | this.CanKeyUp = true; 96 | 97 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 98 | this.CanTouchLeave = true; 99 | 100 | this.drawingCanvas.AddVisual(this); 101 | } 102 | else 103 | return OnTouchLeave(point); 104 | 105 | return true; 106 | } 107 | 108 | public override Boolean OnTouchMove(Point point) 109 | { 110 | var dc = this.RenderOpen(); 111 | 112 | var startPoint = topLeft.Value; 113 | 114 | if (Keyboard.IsKeyDown(Key.LeftShift)) 115 | { 116 | var len = Math.Min(Math.Abs(point.X - startPoint.X), Math.Abs(point.Y - startPoint.Y)); 117 | point = new Point(startPoint.X + (point.X > startPoint.X ? len : -len), startPoint.Y + (point.Y > startPoint.Y ? len : -len)); 118 | } 119 | 120 | if ((startPoint - point).Length <= pen.Thickness) 121 | { 122 | dc.Close(); 123 | return true; 124 | } 125 | 126 | bottomRight = point; 127 | 128 | radiusX = (point.X - startPoint.X) / 2; 129 | radiusY = (point.Y - startPoint.Y) / 2; 130 | center.X = startPoint.X + radiusX; 131 | center.Y = startPoint.Y + radiusY; 132 | 133 | radiusX = Math.Abs(radiusX); 134 | radiusY = Math.Abs(radiusY); 135 | 136 | dc.DrawEllipse(null, pen, center, radiusX, radiusY); 137 | dc.Close(); 138 | 139 | return true; 140 | } 141 | 142 | #endregion 143 | 144 | #region 序列化 145 | 146 | public override DrawGeometrySerializerBase ToSerializer() 147 | { 148 | var serializer = new DrawEllipseSerializer 149 | { 150 | Color = ((SolidColorBrush)pen.Brush).Color, 151 | StrokeThickness = pen.Thickness, 152 | Geometry = geometry.ToString() 153 | }; 154 | 155 | if (geometry.Transform != null) 156 | serializer.Matrix = geometry.Transform.Value; 157 | 158 | return serializer; 159 | } 160 | 161 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 162 | { 163 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 164 | 165 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 166 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 167 | 168 | this.IsFinish = true; 169 | 170 | this.Draw(); 171 | } 172 | 173 | #endregion 174 | 175 | #region 字段 176 | 177 | private Point? topLeft, bottomRight; 178 | private Double radiusX, radiusY; 179 | private Point center; 180 | 181 | #endregion 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /EraserDrawTool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools 8 | { 9 | /// 10 | /// 橡皮擦 11 | /// 12 | public sealed class EraserDrawTool : IDrawTool 13 | { 14 | public EraserDrawTool(DrawingCanvas drawingCanvas) 15 | { 16 | this.drawingCanvas = drawingCanvas; 17 | 18 | // 准备要处理的事件 19 | this.CanTouchDown = true; 20 | } 21 | 22 | #region 公开方法 23 | 24 | public Boolean OnKeyDown(Key key) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public Boolean OnKeyUp(Key key) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public Boolean OnTouchDown(Int32 touchId, Point point) 35 | { 36 | this.TouchId = touchId; 37 | 38 | this.CanTouchDown = false; 39 | 40 | this.pen = this.drawingCanvas.Pen; 41 | this.radius = pen.Thickness / 2; 42 | 43 | this.lastPoint = point; 44 | 45 | this.deleteDrawGeometries = new List(); 46 | 47 | var geometry = new PathGeometry(); 48 | var figure = new PathFigure { StartPoint = new Point(point.X - radius, point.Y - radius), IsClosed = true, IsFilled = true }; 49 | geometry.Figures.Add(figure); 50 | 51 | var line = new LineSegment(new Point(point.X + radius, point.Y - radius), false); 52 | figure.Segments.Add(line); 53 | 54 | line = new LineSegment(new Point(point.X + radius, point.Y + radius), false); 55 | figure.Segments.Add(line); 56 | 57 | line = new LineSegment(new Point(point.X - radius, point.Y + radius), false); 58 | figure.Segments.Add(line); 59 | 60 | if (Erase(geometry)) 61 | { 62 | IsFinish = true; 63 | return true; 64 | } 65 | 66 | this.drawingCanvas.AddWorkingDrawTool(this); 67 | 68 | this.CanTouchMove = true; 69 | this.CanTouchUp = true; 70 | 71 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 72 | this.CanTouchLeave = true; 73 | 74 | this.drawingCanvas.handleCursor = true; 75 | 76 | return true; 77 | } 78 | 79 | public Boolean OnTouchEnter(Point point) 80 | { 81 | throw new NotImplementedException(); 82 | } 83 | 84 | public Boolean OnTouchLeave(Point point) 85 | { 86 | return this.OnTouchUp(point); 87 | } 88 | 89 | public Boolean OnTouchMove(Point point) 90 | { 91 | var geometry = new PathGeometry(); 92 | var figure = new PathFigure { IsClosed = true, IsFilled = true }; 93 | geometry.Figures.Add(figure); 94 | 95 | var positiveX = radius * (point.X > lastPoint.X ? 1 : -1); 96 | var positiveY = radius * (point.Y > lastPoint.Y ? 1 : -1); 97 | 98 | figure.StartPoint = new Point(lastPoint.X - positiveX, lastPoint.Y - positiveY); 99 | 100 | var line = new LineSegment(new Point(lastPoint.X + positiveX, lastPoint.Y - positiveY), false); 101 | figure.Segments.Add(line); 102 | 103 | line = new LineSegment(new Point(point.X + positiveX, point.Y - positiveY), false); 104 | figure.Segments.Add(line); 105 | 106 | line = new LineSegment(new Point(point.X + positiveX, point.Y + positiveY), false); 107 | figure.Segments.Add(line); 108 | 109 | line = new LineSegment(new Point(point.X - positiveX, point.Y + positiveY), false); 110 | figure.Segments.Add(line); 111 | 112 | line = new LineSegment(new Point(lastPoint.X - positiveX, lastPoint.Y + positiveY), false); 113 | figure.Segments.Add(line); 114 | 115 | lastPoint = point; 116 | 117 | if (Erase(geometry)) 118 | OnTouchUp(point); 119 | 120 | return true; 121 | } 122 | 123 | public Boolean OnTouchUp(Point point) 124 | { 125 | this.drawingCanvas.DeleteWorkingDrawTool(this); 126 | 127 | this.IsFinish = true; 128 | 129 | this.CanTouchMove = false; 130 | this.CanTouchUp = false; 131 | this.CanTouchLeave = false; 132 | 133 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 134 | this.drawingCanvas.ReleaseMouseCapture(); 135 | 136 | this.drawingCanvas.handleCursor = false; 137 | 138 | return true; 139 | } 140 | 141 | #endregion 142 | 143 | #region 私有方法 144 | 145 | private Boolean Erase(Geometry geometry) 146 | { 147 | var empty = true; 148 | 149 | foreach (var g in this.drawingCanvas.GetDrawGeometries()) 150 | { 151 | if (g.Erase(geometry)) 152 | deleteDrawGeometries.Add(g); 153 | else 154 | empty = false; 155 | } 156 | 157 | foreach (var g in deleteDrawGeometries) 158 | { 159 | this.drawingCanvas.DeleteVisual(g); 160 | } 161 | 162 | deleteDrawGeometries.Clear(); 163 | 164 | return empty; 165 | } 166 | 167 | #endregion 168 | 169 | #region 属性 170 | 171 | public Int32 TouchId { get; private set; } 172 | 173 | public Boolean CanTouchEnter { get; private set; } 174 | 175 | public Boolean CanTouchLeave { get; private set; } 176 | 177 | public Boolean CanTouchDown { get; private set; } 178 | 179 | public Boolean CanTouchMove { get; private set; } 180 | 181 | public Boolean CanTouchUp { get; private set; } 182 | 183 | public Boolean CanKeyDown { get; private set; } 184 | 185 | public Boolean CanKeyUp { get; private set; } 186 | 187 | public Boolean IsFinish { get; private set; } 188 | 189 | public DrawToolType DrawingToolType => DrawToolType.Eraser; 190 | 191 | #endregion 192 | 193 | #region 字段 194 | 195 | private DrawingCanvas drawingCanvas; 196 | private Point lastPoint; 197 | private Pen pen; 198 | private Double radius; 199 | private List deleteDrawGeometries; 200 | 201 | #endregion 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /IDrawTool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Input; 4 | 5 | namespace DrawTools 6 | { 7 | /// 8 | /// 绘制工具接口 9 | /// 10 | public interface IDrawTool 11 | { 12 | /// 13 | /// 触摸Id,用于分辨多点触摸,0表示鼠标 14 | /// 15 | Int32 TouchId { get; } 16 | 17 | /// 18 | /// 是否可以处理鼠标进入事件 19 | /// 20 | Boolean CanTouchEnter { get; } 21 | 22 | /// 23 | /// 处理鼠标进入事件 24 | /// 25 | /// 相对画布的点 26 | /// 事件是否已处理 27 | Boolean OnTouchEnter(Point point); 28 | 29 | /// 30 | /// 是否可以处理鼠标离开事件 31 | /// 32 | Boolean CanTouchLeave { get; } 33 | 34 | /// 35 | /// 处理鼠标离开事件 36 | /// 37 | /// 相对画布的点 38 | /// 事件是否已处理 39 | Boolean OnTouchLeave(Point point); 40 | 41 | /// 42 | /// 是否可以处理鼠标按下事件 43 | /// 44 | Boolean CanTouchDown { get; } 45 | 46 | /// 47 | /// 执行鼠标按下事件 48 | /// 49 | /// 触摸Id,用于分辨多点触摸,0表示鼠标 50 | /// 相对画布的点 51 | /// 事件是否已处理 52 | Boolean OnTouchDown(Int32 touchId, Point point); 53 | 54 | /// 55 | /// 是否可以处理鼠标移动事件 56 | /// 57 | Boolean CanTouchMove { get; } 58 | 59 | /// 60 | /// 执行鼠标移动事件 61 | /// 62 | /// 相对画布的点 63 | /// 事件是否已处理 64 | Boolean OnTouchMove(Point point); 65 | 66 | /// 67 | /// 是否可以处理鼠标弹起事件 68 | /// 69 | Boolean CanTouchUp { get; } 70 | 71 | /// 72 | /// 执行鼠标弹起事件 73 | /// 74 | /// 相对画布的点 75 | /// 事件是否已处理 76 | Boolean OnTouchUp(Point point); 77 | 78 | /// 79 | /// 是否可以处理键盘按下事件 80 | /// 81 | Boolean CanKeyDown { get; } 82 | 83 | /// 84 | /// 处理键盘按下事件 85 | /// 86 | /// 87 | /// 事件是否已处理 88 | Boolean OnKeyDown(Key key); 89 | 90 | /// 91 | /// 是否可以处理键盘弹起事件 92 | /// 93 | Boolean CanKeyUp { get; } 94 | 95 | /// 96 | /// 处理键盘弹起事件 97 | /// 98 | /// 99 | /// 事件是否已处理 100 | Boolean OnKeyUp(Key key); 101 | 102 | /// 103 | /// 是否结束 104 | /// 105 | Boolean IsFinish { get; } 106 | 107 | /// 108 | /// 画图工具类型 109 | /// 110 | DrawToolType DrawingToolType { get; } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Images/Cursor/hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/Cursor/hand.png -------------------------------------------------------------------------------- /Images/angle1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/angle1.png -------------------------------------------------------------------------------- /Images/angle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/angle2.png -------------------------------------------------------------------------------- /Images/area1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/area1.png -------------------------------------------------------------------------------- /Images/area2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/area2.png -------------------------------------------------------------------------------- /Images/arrow1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/arrow1.png -------------------------------------------------------------------------------- /Images/arrow2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/arrow2.png -------------------------------------------------------------------------------- /Images/circle1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/circle1.png -------------------------------------------------------------------------------- /Images/circle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/circle2.png -------------------------------------------------------------------------------- /Images/clear1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/clear1.png -------------------------------------------------------------------------------- /Images/clear2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/clear2.png -------------------------------------------------------------------------------- /Images/closedCurve1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/closedCurve1.png -------------------------------------------------------------------------------- /Images/closedCurve2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/closedCurve2.png -------------------------------------------------------------------------------- /Images/colors1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/colors1.png -------------------------------------------------------------------------------- /Images/colors2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/colors2.png -------------------------------------------------------------------------------- /Images/curve1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/curve1.png -------------------------------------------------------------------------------- /Images/curve2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/curve2.png -------------------------------------------------------------------------------- /Images/ellipse1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/ellipse1.png -------------------------------------------------------------------------------- /Images/ellipse2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/ellipse2.png -------------------------------------------------------------------------------- /Images/eraser1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/eraser1.png -------------------------------------------------------------------------------- /Images/eraser2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/eraser2.png -------------------------------------------------------------------------------- /Images/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/img1.jpg -------------------------------------------------------------------------------- /Images/line1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/line1.png -------------------------------------------------------------------------------- /Images/line2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/line2.png -------------------------------------------------------------------------------- /Images/pen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/pen1.png -------------------------------------------------------------------------------- /Images/pen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/pen2.png -------------------------------------------------------------------------------- /Images/pointer1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/pointer1.png -------------------------------------------------------------------------------- /Images/pointer2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/pointer2.png -------------------------------------------------------------------------------- /Images/polyline1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/polyline1.png -------------------------------------------------------------------------------- /Images/polyline2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/polyline2.png -------------------------------------------------------------------------------- /Images/ranging1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/ranging1.png -------------------------------------------------------------------------------- /Images/ranging2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/ranging2.png -------------------------------------------------------------------------------- /Images/rectangle1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/rectangle1.png -------------------------------------------------------------------------------- /Images/rectangle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/rectangle2.png -------------------------------------------------------------------------------- /Images/text1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/text1.png -------------------------------------------------------------------------------- /Images/text2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LowPlayer/DrawTools/3c707f30624613df400e4b36ec81e53ef26d5865/Images/text2.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 并不是真的快乐 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LineDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Media; 5 | 6 | namespace DrawTools 7 | { 8 | /// 9 | /// 直线 10 | /// 11 | public sealed class LineDrawTool : DrawGeometryBase 12 | { 13 | public LineDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 14 | { 15 | this.DrawingToolType = DrawToolType.Line; 16 | 17 | // 准备要处理的事件 18 | this.CanTouchDown = true; 19 | } 20 | 21 | #region 鼠标键盘事件 22 | 23 | public override Boolean OnTouchLeave(Point point) 24 | { 25 | if (!endPoint.HasValue) 26 | this.drawingCanvas.DeleteVisual(this); 27 | else 28 | { 29 | geometry = geometry.GetWidenedPathGeometry(pen); 30 | Draw(); 31 | } 32 | 33 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 34 | this.drawingCanvas.ReleaseMouseCapture(); 35 | 36 | this.drawingCanvas.DeleteWorkingDrawTool(this); 37 | 38 | this.IsFinish = true; 39 | 40 | this.CanKeyDown = false; 41 | this.CanTouchMove = false; 42 | this.CanTouchLeave = false; 43 | 44 | return true; 45 | } 46 | 47 | public override Boolean OnTouchDown(Int32 touchId, Point point) 48 | { 49 | this.TouchId = touchId; 50 | 51 | if (!startPoint.HasValue) 52 | { 53 | this.drawingCanvas.AddWorkingDrawTool(this); 54 | 55 | this.pen = this.drawingCanvas.Pen; 56 | 57 | startPoint = point; 58 | 59 | this.geometry = new PathGeometry(); 60 | 61 | var figure = new PathFigure { StartPoint = point }; 62 | pathGeometry.Figures.Add(figure); 63 | 64 | this.CanTouchMove = true; 65 | 66 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 67 | this.CanTouchLeave = true; 68 | 69 | this.drawingCanvas.AddVisual(this); 70 | 71 | return true; 72 | } 73 | else 74 | return OnTouchLeave(point); 75 | } 76 | 77 | public override Boolean OnTouchMove(Point point) 78 | { 79 | var start = startPoint.Value; 80 | 81 | if ((start - point).Length < pen.Thickness) 82 | return true; 83 | 84 | var dc = this.RenderOpen(); 85 | 86 | var figure = pathGeometry.Figures[0]; 87 | var line = new LineSegment(point, true); 88 | 89 | if (endPoint.HasValue) 90 | figure.Segments[0] = line; 91 | else 92 | figure.Segments.Add(line); 93 | 94 | endPoint = point; 95 | 96 | dc.DrawGeometry(null, pen, geometry); 97 | dc.Close(); 98 | 99 | return true; 100 | } 101 | 102 | #endregion 103 | 104 | #region 序列化 105 | 106 | public override DrawGeometrySerializerBase ToSerializer() 107 | { 108 | var serializer = new DrawLineSerializer 109 | { 110 | Color = ((SolidColorBrush)pen.Brush).Color, 111 | StrokeThickness = pen.Thickness, 112 | Geometry = geometry.ToString(), 113 | StartPoint = startPoint.Value, 114 | EndPoint = endPoint.Value 115 | }; 116 | 117 | if (geometry.Transform != null) 118 | serializer.Matrix = geometry.Transform.Value; 119 | 120 | return serializer; 121 | } 122 | 123 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 124 | { 125 | var lineSerializer = (DrawLineSerializer)serializer; 126 | 127 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 128 | 129 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 130 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 131 | 132 | this.startPoint = lineSerializer.StartPoint; 133 | this.endPoint = lineSerializer.EndPoint; 134 | 135 | this.IsFinish = true; 136 | 137 | this.Draw(); 138 | } 139 | 140 | #endregion 141 | 142 | #region 字段 143 | 144 | private Point? startPoint, endPoint; 145 | 146 | #endregion 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /PenDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | 8 | namespace DrawTools 9 | { 10 | /// 11 | /// 画笔 12 | /// 13 | public sealed class PenDrawTool : DrawGeometryBase 14 | { 15 | public PenDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 16 | { 17 | this.DrawingToolType = DrawToolType.Pen; 18 | 19 | // 准备要处理的事件 20 | this.CanTouchDown = true; 21 | } 22 | 23 | #region 鼠标键盘事件 24 | 25 | public override Boolean OnTouchDown(Int32 touchId, Point point) 26 | { 27 | this.TouchId = touchId; 28 | 29 | this.drawingCanvas.AddWorkingDrawTool(this); 30 | this.drawingCanvas.AddVisual(this); 31 | this.drawingCanvas.handleCursor = true; 32 | 33 | this.CanTouchDown = false; 34 | 35 | this.pen = this.drawingCanvas.Pen; 36 | 37 | points.Add(point); 38 | 39 | geometry = new PathGeometry(); 40 | 41 | var figure = new PathFigure { StartPoint = point }; 42 | pathGeometry.Figures.Add(figure); 43 | 44 | this.CanTouchMove = true; 45 | this.CanTouchUp = true; 46 | 47 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 48 | this.CanTouchLeave = true; 49 | 50 | return true; 51 | } 52 | 53 | public override Boolean OnTouchMove(Point point) 54 | { 55 | if ((point - points.Last()).Length < pen.Thickness) 56 | return true; 57 | 58 | points.Add(point); 59 | 60 | var figure = pathGeometry.Figures[0]; 61 | var line = new LineSegment(point, true) { IsSmoothJoin = true }; 62 | figure.Segments.Add(line); 63 | 64 | var dc = this.RenderOpen(); 65 | dc.DrawGeometry(null, pen, geometry); 66 | dc.Close(); 67 | 68 | return true; 69 | } 70 | 71 | public override Boolean OnTouchUp(Point point) 72 | { 73 | if (points.Count < 2) 74 | this.drawingCanvas.DeleteVisual(this); 75 | else 76 | { 77 | geometry = geometry.GetWidenedPathGeometry(pen); 78 | Draw(); 79 | } 80 | 81 | this.drawingCanvas.DeleteWorkingDrawTool(this); 82 | 83 | this.IsFinish = true; 84 | 85 | this.CanTouchMove = false; 86 | this.CanTouchUp = false; 87 | this.CanTouchLeave = false; 88 | 89 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 90 | this.drawingCanvas.ReleaseMouseCapture(); 91 | 92 | this.drawingCanvas.handleCursor = false; 93 | 94 | return true; 95 | } 96 | 97 | #endregion 98 | 99 | #region 序列化 100 | 101 | public override DrawGeometrySerializerBase ToSerializer() 102 | { 103 | var serializer = new DrawPenSerializer 104 | { 105 | Color = ((SolidColorBrush)pen.Brush).Color, 106 | StrokeThickness = pen.Thickness, 107 | Geometry = geometry.ToString() 108 | }; 109 | 110 | if (geometry.Transform != null) 111 | serializer.Matrix = geometry.Transform.Value; 112 | 113 | return serializer; 114 | } 115 | 116 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 117 | { 118 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 119 | 120 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 121 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 122 | 123 | this.IsFinish = true; 124 | 125 | this.Draw(); 126 | } 127 | 128 | #endregion 129 | 130 | #region 字段 131 | 132 | private List points = new List(); 133 | 134 | #endregion 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /PointerDrawTool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools 8 | { 9 | /// 10 | /// 指针(拾取) 11 | /// 12 | public sealed class PointerDrawTool : DrawingVisual, IDrawTool 13 | { 14 | public PointerDrawTool(DrawingCanvas drawingCanvas) 15 | { 16 | this.drawingCanvas = drawingCanvas; 17 | 18 | // 准备要处理的事件 19 | this.CanTouchDown = true; 20 | 21 | this.selectedDrawGeometries = new List(); 22 | } 23 | 24 | #region 公开方法 25 | 26 | public Boolean OnKeyDown(Key key) 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | 31 | public Boolean OnKeyUp(Key key) 32 | { 33 | throw new NotImplementedException(); 34 | } 35 | 36 | public Boolean OnTouchDown(Int32 touchId, Point point) 37 | { 38 | if (this.drawingCanvas.DrawingToolType != this.DrawingToolType) 39 | { 40 | Delete(); 41 | return false; 42 | } 43 | 44 | this.TouchId = touchId; 45 | 46 | this.mode = 0; 47 | 48 | if (this.VisualParent == null) 49 | { 50 | this.drawingCanvas.AddWorkingDrawTool(this); 51 | this.drawingCanvas.Insert(0, this); 52 | } 53 | else 54 | { 55 | this.drawingCanvas.MovoToHead(this); 56 | 57 | var visual = this.drawingCanvas.GetVisual(point); 58 | 59 | if (visual == this || (visual is DrawGeometryBase draw && selectedDrawGeometries.Contains(draw))) 60 | { 61 | this.mode = 1; 62 | this.lastPoint = point; 63 | } 64 | } 65 | 66 | this.startPoint = point; 67 | 68 | if (this.mode == 0) 69 | { 70 | selectRect = drawRect = null; 71 | 72 | this.selectedDrawGeometries.Clear(); 73 | 74 | foreach (var draw in this.drawingCanvas.GetDrawGeometries()) 75 | { 76 | if (draw.Select(point)) 77 | { 78 | if (selectRect.HasValue) 79 | selectRect = Rect.Union(selectRect.Value, draw.Selected()); 80 | else 81 | selectRect = draw.Selected(); 82 | 83 | selectedDrawGeometries.Add(draw); 84 | } 85 | else 86 | draw.Unselected(); 87 | } 88 | 89 | if (this.geometry == null) 90 | { 91 | this.geometry = new PathGeometry(); 92 | var figure = new PathFigure { StartPoint = point, IsClosed = true, IsFilled = true }; 93 | geometry.Figures.Add(figure); 94 | } 95 | else 96 | geometry.Figures[0].StartPoint = point; 97 | } 98 | 99 | this.CanTouchMove = true; 100 | this.CanTouchUp = true; 101 | 102 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 103 | this.CanTouchLeave = true; 104 | 105 | return true; 106 | } 107 | 108 | public Boolean OnTouchEnter(Point point) 109 | { 110 | throw new NotImplementedException(); 111 | } 112 | 113 | public Boolean OnTouchLeave(Point point) 114 | { 115 | return OnTouchUp(point); 116 | } 117 | 118 | public Boolean OnTouchMove(Point point) 119 | { 120 | if (mode == 0) 121 | { 122 | var figure = geometry.Figures[0]; 123 | 124 | var topRight = new Point(point.X, startPoint.Y); 125 | var bottomLeft = new Point(startPoint.X, point.Y); 126 | 127 | if (figure.Segments.Count == 0) 128 | { 129 | var line = new LineSegment(topRight, false); 130 | figure.Segments.Add(line); 131 | 132 | line = new LineSegment(point, false); 133 | figure.Segments.Add(line); 134 | 135 | line = new LineSegment(bottomLeft, false); 136 | figure.Segments.Add(line); 137 | } 138 | else 139 | { 140 | var line = (LineSegment)figure.Segments[0]; 141 | line.Point = topRight; 142 | 143 | line = (LineSegment)figure.Segments[1]; 144 | line.Point = point; 145 | 146 | line = (LineSegment)figure.Segments[2]; 147 | line.Point = bottomLeft; 148 | } 149 | 150 | selectedDrawGeometries.Clear(); 151 | 152 | foreach (var draw in this.drawingCanvas.GetDrawGeometries()) 153 | { 154 | if (draw.Select(geometry)) 155 | { 156 | if (selectRect.HasValue) 157 | selectRect = Rect.Union(selectRect.Value, draw.Selected()); 158 | else 159 | selectRect = draw.Selected(); 160 | 161 | selectedDrawGeometries.Add(draw); 162 | } 163 | else 164 | draw.Unselected(); 165 | } 166 | 167 | drawRect = new Rect(startPoint, point); 168 | 169 | var dc = this.RenderOpen(); 170 | dc.DrawRectangle(null, this.drawingCanvas.SelectBackgroundPen, drawRect.Value); 171 | dc.DrawRectangle(null, this.drawingCanvas.SelectPen, drawRect.Value); 172 | dc.Close(); 173 | } 174 | else if (mode == 1) 175 | { 176 | // 移动 177 | var dx = point.X - lastPoint.X; 178 | var dy = point.Y - lastPoint.Y; 179 | 180 | lastPoint = point; 181 | 182 | foreach (var draw in selectedDrawGeometries) 183 | { 184 | draw.Move(dx, dy); 185 | } 186 | 187 | var rect = selectRect.Value; 188 | rect.X += dx; 189 | rect.Y += dy; 190 | selectRect = rect; 191 | 192 | var dc = this.RenderOpen(); 193 | dc.DrawRectangle(Brushes.Transparent, this.drawingCanvas.SelectBackgroundPen, selectRect.Value); 194 | dc.DrawRectangle(null, this.drawingCanvas.SelectPen, selectRect.Value); 195 | dc.Close(); 196 | } 197 | 198 | return true; 199 | } 200 | 201 | public Boolean OnTouchUp(Point point) 202 | { 203 | if (mode == 0) 204 | { 205 | if (selectedDrawGeometries.Count == 0) 206 | { 207 | Delete(); 208 | return true; 209 | } 210 | 211 | if (drawRect.HasValue || selectedDrawGeometries.Count > 1) 212 | { 213 | if (drawRect.HasValue) 214 | selectRect = Rect.Union(selectRect.Value, drawRect.Value); 215 | 216 | var dc = this.RenderOpen(); 217 | dc.DrawRectangle(Brushes.Transparent, this.drawingCanvas.SelectBackgroundPen, selectRect.Value); 218 | dc.DrawRectangle(null, this.drawingCanvas.SelectPen, selectRect.Value); 219 | dc.Close(); 220 | } 221 | } 222 | else if (mode == 1 && startPoint == lastPoint) 223 | { 224 | var visual = this.drawingCanvas.GetVisual(point); 225 | 226 | if (visual is DrawGeometryBase selectDraw && selectDraw.CanEdit) 227 | { 228 | selectDraw.Edit(); 229 | 230 | foreach (var draw in selectedDrawGeometries) 231 | { 232 | if (draw != selectDraw) 233 | draw.Unselected(); 234 | } 235 | 236 | selectedDrawGeometries.Clear(); 237 | 238 | Delete(); 239 | return true; 240 | } 241 | } 242 | 243 | this.CanTouchMove = false; 244 | this.CanTouchUp = false; 245 | this.CanTouchLeave = false; 246 | 247 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 248 | this.drawingCanvas.ReleaseMouseCapture(); 249 | 250 | return true; 251 | } 252 | 253 | #endregion 254 | 255 | #region 私有方法 256 | 257 | private void Delete() 258 | { 259 | foreach (var draw in selectedDrawGeometries) 260 | { 261 | draw.Unselected(); 262 | } 263 | 264 | selectedDrawGeometries.Clear(); 265 | 266 | this.drawingCanvas.DeleteVisual(this); 267 | this.drawingCanvas.DeleteWorkingDrawTool(this); 268 | 269 | IsFinish = true; 270 | 271 | this.CanTouchDown = false; 272 | this.CanTouchMove = false; 273 | this.CanTouchUp = false; 274 | this.CanTouchLeave = false; 275 | 276 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 277 | this.drawingCanvas.ReleaseMouseCapture(); 278 | } 279 | 280 | #endregion 281 | 282 | #region 属性 283 | 284 | public Int32 TouchId { get; private set; } 285 | 286 | public Boolean CanTouchEnter { get; private set; } 287 | 288 | public Boolean CanTouchLeave { get; private set; } 289 | 290 | public Boolean CanTouchDown { get; private set; } 291 | 292 | public Boolean CanTouchMove { get; private set; } 293 | 294 | public Boolean CanTouchUp { get; private set; } 295 | 296 | public Boolean CanKeyDown { get; private set; } 297 | 298 | public Boolean CanKeyUp { get; private set; } 299 | 300 | public Boolean IsFinish { get; private set; } 301 | 302 | public DrawToolType DrawingToolType => DrawToolType.Pointer; 303 | 304 | #endregion 305 | 306 | #region 字段 307 | 308 | private DrawingCanvas drawingCanvas; 309 | private Point startPoint, lastPoint; 310 | private List selectedDrawGeometries; 311 | private PathGeometry geometry; 312 | private Rect? selectRect, drawRect; 313 | /// 314 | /// 0:拾取|1:拖动 315 | /// 316 | private Int32 mode; 317 | 318 | #endregion 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /PolygonDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | 8 | namespace DrawTools 9 | { 10 | /// 11 | /// 多边形 12 | /// 13 | public sealed class PolygonDrawTool : DrawGeometryBase 14 | { 15 | public PolygonDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 16 | { 17 | this.DrawingToolType = DrawToolType.Polygon; 18 | 19 | // 准备要处理的事件 20 | this.CanTouchDown = true; 21 | } 22 | 23 | #region 鼠标键盘事件 24 | 25 | public override Boolean OnTouchLeave(Point point) 26 | { 27 | if (mousePoint.HasValue) 28 | { 29 | points.Add(mousePoint.Value); 30 | mousePoint = null; 31 | } 32 | 33 | if (points.Count < 3) 34 | this.drawingCanvas.DeleteVisual(this); 35 | else 36 | { 37 | var figure = pathGeometry.Figures[0]; 38 | figure.IsClosed = true; 39 | 40 | geometry = geometry.GetWidenedPathGeometry(pen); 41 | Draw(); 42 | } 43 | 44 | this.drawingCanvas.DeleteWorkingDrawTool(this); 45 | 46 | this.IsFinish = true; 47 | 48 | this.CanTouchDown = false; 49 | this.CanTouchMove = false; 50 | this.CanTouchLeave = false; 51 | 52 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 53 | this.drawingCanvas.ReleaseMouseCapture(); 54 | 55 | return true; 56 | } 57 | 58 | public override Boolean OnTouchDown(Int32 touchId, Point point) 59 | { 60 | this.TouchId = touchId; 61 | 62 | if (points.Count == 0) 63 | { 64 | this.drawingCanvas.AddWorkingDrawTool(this); 65 | 66 | this.pen = this.drawingCanvas.Pen; 67 | 68 | points.Add(point); 69 | 70 | geometry = new PathGeometry(); 71 | 72 | var figure = new PathFigure { StartPoint = point }; 73 | pathGeometry.Figures.Add(figure); 74 | 75 | this.CanTouchMove = true; 76 | 77 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 78 | this.CanTouchLeave = true; 79 | 80 | this.drawingCanvas.AddVisual(this); 81 | } 82 | else if ((point - points.Last()).Length <= pen.Thickness) 83 | return OnTouchLeave(point); 84 | else if (mousePoint.HasValue) 85 | { 86 | points.Add(mousePoint.Value); 87 | mousePoint = null; 88 | } 89 | 90 | return true; 91 | } 92 | 93 | public override Boolean OnTouchMove(Point point) 94 | { 95 | if ((point - points.Last()).Length <= pen.Thickness) 96 | return true; 97 | 98 | var figure = pathGeometry.Figures[0]; 99 | var line = new LineSegment(point, true) { IsSmoothJoin = true }; 100 | 101 | if (mousePoint.HasValue) 102 | figure.Segments[figure.Segments.Count - 1] = line; 103 | else 104 | figure.Segments.Add(line); 105 | 106 | mousePoint = point; 107 | 108 | var dc = this.RenderOpen(); 109 | dc.DrawGeometry(null, pen, geometry); 110 | dc.Close(); 111 | 112 | return true; 113 | } 114 | 115 | #endregion 116 | 117 | #region 序列化 118 | 119 | public override DrawGeometrySerializerBase ToSerializer() 120 | { 121 | var serializer = new DrawPolygonSerializer 122 | { 123 | Color = ((SolidColorBrush)pen.Brush).Color, 124 | StrokeThickness = pen.Thickness, 125 | Geometry = geometry.ToString() 126 | }; 127 | 128 | if (geometry.Transform != null) 129 | serializer.Matrix = geometry.Transform.Value; 130 | 131 | return serializer; 132 | } 133 | 134 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 135 | { 136 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 137 | 138 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 139 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 140 | 141 | this.IsFinish = true; 142 | 143 | this.Draw(); 144 | } 145 | 146 | #endregion 147 | 148 | #region 字段 149 | 150 | private List points = new List(); 151 | private Point? mousePoint; 152 | 153 | #endregion 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /PolylineDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | 8 | namespace DrawTools 9 | { 10 | /// 11 | /// 折线 12 | /// 13 | public sealed class PolylineDrawTool : DrawGeometryBase 14 | { 15 | public PolylineDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 16 | { 17 | this.DrawingToolType = DrawToolType.Polyline; 18 | 19 | // 准备要处理的事件 20 | this.CanTouchDown = true; 21 | } 22 | 23 | #region 鼠标键盘事件 24 | 25 | public override Boolean OnTouchLeave(Point point) 26 | { 27 | if (mousePoint.HasValue) 28 | { 29 | points.Add(mousePoint.Value); 30 | mousePoint = null; 31 | } 32 | 33 | if (points.Count < 2) 34 | this.drawingCanvas.DeleteVisual(this); 35 | else 36 | { 37 | geometry = geometry.GetWidenedPathGeometry(pen); 38 | Draw(); 39 | } 40 | 41 | this.drawingCanvas.DeleteWorkingDrawTool(this); 42 | 43 | this.IsFinish = true; 44 | 45 | this.CanTouchDown = false; 46 | this.CanTouchMove = false; 47 | this.CanTouchLeave = false; 48 | 49 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 50 | this.drawingCanvas.ReleaseMouseCapture(); 51 | 52 | return true; 53 | } 54 | 55 | public override Boolean OnTouchDown(Int32 touchId, Point point) 56 | { 57 | this.TouchId = touchId; 58 | 59 | if (points.Count == 0) 60 | { 61 | this.drawingCanvas.AddWorkingDrawTool(this); 62 | 63 | this.pen = this.drawingCanvas.Pen; 64 | 65 | points.Add(point); 66 | 67 | geometry = new PathGeometry(); 68 | 69 | var figure = new PathFigure { StartPoint = point }; 70 | pathGeometry.Figures.Add(figure); 71 | 72 | this.CanTouchMove = true; 73 | 74 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 75 | this.CanTouchLeave = true; 76 | 77 | this.drawingCanvas.AddVisual(this); 78 | } 79 | else if ((point - points.Last()).Length <= pen.Thickness) 80 | OnTouchLeave(point); 81 | else if (mousePoint.HasValue) 82 | { 83 | points.Add(mousePoint.Value); 84 | mousePoint = null; 85 | } 86 | 87 | return true; 88 | } 89 | 90 | public override Boolean OnTouchMove(Point point) 91 | { 92 | if ((point - points.Last()).Length <= pen.Thickness) 93 | return true; 94 | 95 | var figure = pathGeometry.Figures[0]; 96 | var line = new LineSegment(point, true) { IsSmoothJoin = true }; 97 | 98 | if (mousePoint.HasValue) 99 | figure.Segments[figure.Segments.Count - 1] = line; 100 | else 101 | figure.Segments.Add(line); 102 | 103 | mousePoint = point; 104 | 105 | var dc = this.RenderOpen(); 106 | dc.DrawGeometry(null, pen, geometry); 107 | dc.Close(); 108 | 109 | return true; 110 | } 111 | 112 | #endregion 113 | 114 | #region 序列化 115 | 116 | public override DrawGeometrySerializerBase ToSerializer() 117 | { 118 | var serializer = new DrawPolylineSerializer 119 | { 120 | Color = ((SolidColorBrush)pen.Brush).Color, 121 | StrokeThickness = pen.Thickness, 122 | Geometry = geometry.ToString() 123 | }; 124 | 125 | if (geometry.Transform != null) 126 | serializer.Matrix = geometry.Transform.Value; 127 | 128 | return serializer; 129 | } 130 | 131 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 132 | { 133 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 134 | 135 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 136 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 137 | 138 | this.IsFinish = true; 139 | 140 | this.Draw(); 141 | } 142 | 143 | #endregion 144 | 145 | #region 字段 146 | 147 | private List points = new List(); 148 | private Point? mousePoint; 149 | 150 | #endregion 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | // 有关程序集的一般信息由以下 8 | // 控制。更改这些特性值可修改 9 | // 与程序集关联的信息。 10 | [assembly: AssemblyTitle("DrawTools")] 11 | [assembly: AssemblyDescription("")] 12 | [assembly: AssemblyConfiguration("")] 13 | [assembly: AssemblyCompany("")] 14 | [assembly: AssemblyProduct("DrawTools")] 15 | [assembly: AssemblyCopyright("Copyright © 2020")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyCulture("")] 18 | 19 | // 将 ComVisible 设置为 false 会使此程序集中的类型 20 | //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 21 | //请将此类型的 ComVisible 特性设置为 true。 22 | [assembly: ComVisible(false)] 23 | 24 | //若要开始生成可本地化的应用程序,请设置 25 | //.csproj 文件中的 CultureYouAreCodingWith 26 | //例如,如果您在源文件中使用的是美国英语, 27 | //使用的是美国英语,请将 设置为 en-US。 然后取消 28 | //对以下 NeutralResourceLanguage 特性的注释。 更新 29 | //以下行中的“en-US”以匹配项目文件中的 UICulture 设置。 30 | 31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 32 | 33 | 34 | [assembly: ThemeInfo( 35 | ResourceDictionaryLocation.None, //主题特定资源词典所处位置 36 | //(未在页面中找到资源时使用, 37 | //或应用程序资源字典中找到时使用) 38 | ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置 39 | //(未在页面中找到资源时使用, 40 | //、应用程序或任何主题专用资源字典中找到时使用) 41 | )] 42 | 43 | 44 | // 程序集的版本信息由下列四个值组成: 45 | // 46 | // 主版本 47 | // 次版本 48 | // 生成号 49 | // 修订号 50 | // 51 | //可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 52 | //通过使用 "*",如下所示: 53 | // [assembly: AssemblyVersion("1.0.*")] 54 | [assembly: AssemblyVersion("1.0.0.0")] 55 | [assembly: AssemblyFileVersion("1.0.0.0")] 56 | -------------------------------------------------------------------------------- /Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本: 4.0.30319.42000 5 | // 6 | // 对此文件的更改可能导致不正确的行为,如果 7 | // 重新生成代码,则所做更改将丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DrawTools.Properties 12 | { 13 | 14 | 15 | /// 16 | /// 强类型资源类,用于查找本地化字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或删除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// 返回此类使用的缓存 ResourceManager 实例。 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DrawTools.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// 覆盖当前线程的 CurrentUICulture 属性 56 | /// 使用此强类型的资源类的资源查找。 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 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 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace DrawTools.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /RangingDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using DrawTools.Utils; 3 | using System; 4 | using System.Windows; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools 8 | { 9 | public sealed class RangingDrawTool : DrawGeometryBase 10 | { 11 | public RangingDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 12 | { 13 | this.DrawingToolType = DrawToolType.Ranging; 14 | 15 | // 准备要处理的事件 16 | this.CanTouchDown = true; 17 | } 18 | 19 | #region 鼠标键盘事件 20 | 21 | public override Boolean OnTouchLeave(Point point) 22 | { 23 | if (!endPoint.HasValue || (startPoint.Value - endPoint.Value).Length < pen.Thickness) 24 | this.drawingCanvas.DeleteVisual(this); 25 | else 26 | { 27 | var textGeometry = formattedText.BuildGeometry(textPoint); 28 | textGeometry.Transform = new RotateTransform(angle, center.X, center.Y); 29 | geometry = geometry.GetWidenedPathGeometry(pen); 30 | geometry = Geometry.Combine(geometry, textGeometry, GeometryCombineMode.Union, null); 31 | 32 | Draw(); 33 | } 34 | 35 | this.drawingCanvas.DeleteWorkingDrawTool(this); 36 | 37 | this.IsFinish = true; 38 | 39 | this.CanKeyDown = false; 40 | this.CanTouchMove = false; 41 | this.CanTouchLeave = false; 42 | 43 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 44 | this.drawingCanvas.ReleaseMouseCapture(); 45 | 46 | return true; 47 | } 48 | 49 | public override Boolean OnTouchDown(Int32 touchId, Point point) 50 | { 51 | this.TouchId = touchId; 52 | 53 | if (!startPoint.HasValue) 54 | { 55 | this.drawingCanvas.AddWorkingDrawTool(this); 56 | 57 | this.pen = this.drawingCanvas.Pen; 58 | 59 | this.fontSize = this.drawingCanvas.FontSize; 60 | this.typeface = new Typeface(new FontFamily("Microsoft YaHei UI,Tahoma"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); 61 | 62 | this.dpi = DpiHelper.GetDpiFromVisual(this.drawingCanvas); 63 | 64 | startPoint = point; 65 | 66 | geometry = new PathGeometry(); 67 | 68 | var figure = new PathFigure(); 69 | pathGeometry.Figures.Add(figure); 70 | 71 | this.CanTouchMove = true; 72 | 73 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 74 | this.CanTouchLeave = true; 75 | 76 | this.drawingCanvas.AddVisual(this); 77 | 78 | return true; 79 | } 80 | else 81 | return OnTouchLeave(point); 82 | } 83 | 84 | public override Boolean OnTouchMove(Point point) 85 | { 86 | var start = startPoint.Value; 87 | 88 | if ((start - point).Length < this.drawingCanvas.StrokeThickness) 89 | return true; 90 | 91 | var dc = this.RenderOpen(); 92 | 93 | endPoint = point; 94 | 95 | var x = Math.Abs(point.X - start.X) / Dpi.Cm2Wpf; 96 | var y = Math.Abs(point.Y - start.Y) / Dpi.Cm2Wpf; 97 | var len = Math.Sqrt(x * x + y * y); 98 | var text = (len * 10).ToString("0.00") + "mm"; 99 | 100 | formattedText = new FormattedText( 101 | text, 102 | System.Globalization.CultureInfo.InvariantCulture, 103 | FlowDirection.LeftToRight, 104 | typeface, 105 | this.fontSize, 106 | pen.Brush); 107 | 108 | center = new Point((start.X + point.X) / 2, (start.Y + point.Y) / 2); 109 | var width = text.Length * fontSize / 2; // 文字宽度 110 | textPoint = new Point(center.X - width / 2, center.Y - fontSize * 1.2 - pen.Thickness); // 文字左上角,1.2倍行高 111 | 112 | Double? k = null; // 斜率 113 | 114 | if (start.X == point.X) 115 | angle = start.Y > point.Y ? 90 : -90; 116 | else 117 | { 118 | k = (point.Y - start.Y) / (point.X - start.X); 119 | angle = Math.Atan(k.Value) / Math.PI * 180; 120 | } 121 | 122 | dc.PushTransform(new RotateTransform(angle, center.X, center.Y)); 123 | dc.DrawText(formattedText, textPoint); 124 | dc.Pop(); 125 | 126 | var tangentK = k.HasValue ? (-1 / k) : null; 127 | var tangentLen = pen.Thickness + fontSize * 1.2; 128 | 129 | if (tangentK.HasValue) 130 | { 131 | var offsetX1 = Math.Sqrt(tangentLen * tangentLen / (1 + tangentK.Value * tangentK.Value)) * (angle > 0 ? 1 : -1); 132 | 133 | tangent1.X = offsetX1 + start.X; 134 | tangent1.Y = start.Y + offsetX1 * tangentK.Value; 135 | 136 | tangent2.X = offsetX1 + point.X; 137 | tangent2.Y = point.Y + offsetX1 * tangentK.Value; 138 | } 139 | else 140 | { 141 | tangent1.X = start.X + (angle == 90 ? tangentLen : -tangentLen); 142 | tangent1.Y = start.Y; 143 | 144 | tangent2.X = point.X + (angle == 90 ? tangentLen : -tangentLen); 145 | tangent2.Y = point.Y; 146 | } 147 | 148 | var figure = pathGeometry.Figures[0]; 149 | figure.StartPoint = tangent1; 150 | figure.Segments.Clear(); 151 | 152 | var line = new LineSegment(start, true) { IsSmoothJoin = true }; 153 | figure.Segments.Add(line); 154 | line = new LineSegment(point, true) { IsSmoothJoin = true }; 155 | figure.Segments.Add(line); 156 | line = new LineSegment(tangent2, true) { IsSmoothJoin = true }; 157 | figure.Segments.Add(line); 158 | 159 | dc.DrawGeometry(null, pen, geometry); 160 | dc.Close(); 161 | 162 | return true; 163 | } 164 | 165 | #endregion 166 | 167 | #region 序列化 168 | 169 | public override DrawGeometrySerializerBase ToSerializer() 170 | { 171 | var serializer = new DrawRangingSerializer 172 | { 173 | Color = ((SolidColorBrush)pen.Brush).Color, 174 | StrokeThickness = pen.Thickness, 175 | Geometry = geometry.ToString() 176 | }; 177 | 178 | if (geometry.Transform != null) 179 | serializer.Matrix = geometry.Transform.Value; 180 | 181 | return serializer; 182 | } 183 | 184 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 185 | { 186 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 187 | 188 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 189 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 190 | 191 | this.IsFinish = true; 192 | 193 | this.Draw(); 194 | } 195 | 196 | #endregion 197 | 198 | #region 字段 199 | 200 | private Point? startPoint, endPoint; 201 | private Point center, textPoint, tangent1, tangent2; 202 | private Double fontSize; 203 | private Typeface typeface; 204 | private FormattedText formattedText; 205 | private Double angle; 206 | private Dpi dpi; 207 | 208 | #endregion 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /RectangleDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools 8 | { 9 | /// 10 | /// 矩形 11 | /// 12 | public sealed class RectangleDrawTool : DrawGeometryBase 13 | { 14 | public RectangleDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 15 | { 16 | this.DrawingToolType = DrawToolType.Rectangle; 17 | 18 | // 准备要处理的事件 19 | this.CanTouchDown = true; 20 | } 21 | 22 | #region 鼠标键盘事件 23 | 24 | public override Boolean OnKeyDown(Key key) 25 | { 26 | if (key != Key.LeftShift || !bottomRight.HasValue) 27 | return false; 28 | 29 | return OnTouchMove(bottomRight.Value); 30 | } 31 | 32 | public override Boolean OnKeyUp(Key key) 33 | { 34 | if (key != Key.LeftShift || !bottomRight.HasValue) 35 | return false; 36 | 37 | return OnTouchMove(bottomRight.Value); 38 | } 39 | 40 | public override Boolean OnTouchLeave(Point point) 41 | { 42 | if (!bottomRight.HasValue) 43 | this.drawingCanvas.DeleteVisual(this); 44 | else 45 | { 46 | var figure = pathGeometry.Figures[0]; 47 | 48 | var line = new LineSegment(new Point(bottomRight.Value.X, topLeft.Value.Y), true); 49 | figure.Segments.Add(line); 50 | 51 | line = new LineSegment(bottomRight.Value, true); 52 | figure.Segments.Add(line); 53 | 54 | line = new LineSegment(new Point(topLeft.Value.X, bottomRight.Value.Y), true); 55 | figure.Segments.Add(line); 56 | 57 | geometry = geometry.GetWidenedPathGeometry(pen); 58 | 59 | Draw(); 60 | } 61 | 62 | this.drawingCanvas.DeleteWorkingDrawTool(this); 63 | 64 | this.IsFinish = true; 65 | 66 | this.CanTouchDown = false; 67 | this.CanTouchMove = false; 68 | this.CanTouchLeave = false; 69 | this.CanKeyDown = false; 70 | this.CanKeyUp = false; 71 | 72 | if (this.TouchId == 0 && this.drawingCanvas.IsMouseCaptured) 73 | this.drawingCanvas.ReleaseMouseCapture(); 74 | 75 | return true; 76 | } 77 | 78 | public override Boolean OnTouchDown(Int32 touchId, Point point) 79 | { 80 | this.TouchId = touchId; 81 | 82 | if (!topLeft.HasValue) 83 | { 84 | this.drawingCanvas.AddWorkingDrawTool(this); 85 | 86 | this.pen = this.drawingCanvas.Pen; 87 | 88 | topLeft = point; 89 | 90 | this.geometry = new PathGeometry(); 91 | 92 | var figure = new PathFigure { StartPoint = point, IsClosed = true }; 93 | pathGeometry.Figures.Add(figure); 94 | 95 | this.CanTouchMove = true; 96 | this.CanKeyDown = true; 97 | this.CanKeyUp = true; 98 | 99 | if (this.TouchId != 0 || !this.drawingCanvas.CaptureMouse()) 100 | this.CanTouchLeave = true; 101 | 102 | this.drawingCanvas.AddVisual(this); 103 | } 104 | else 105 | return OnTouchLeave(point); 106 | 107 | return true; 108 | } 109 | 110 | public override Boolean OnTouchMove(Point point) 111 | { 112 | var dc = this.RenderOpen(); 113 | 114 | var startPoint = topLeft.Value; 115 | 116 | if (Keyboard.IsKeyDown(Key.LeftShift)) 117 | { 118 | var len = Math.Min(Math.Abs(point.X - startPoint.X), Math.Abs(point.Y - startPoint.Y)); 119 | point = new Point(startPoint.X + (point.X > startPoint.X ? len : -len), startPoint.Y + (point.Y > startPoint.Y ? len : -len)); 120 | } 121 | 122 | if ((startPoint - point).Length > pen.Thickness) 123 | { 124 | bottomRight = point; 125 | dc.DrawRectangle(null, pen, new Rect(startPoint, point)); 126 | } 127 | 128 | dc.Close(); 129 | 130 | return true; 131 | } 132 | 133 | #endregion 134 | 135 | #region 序列化 136 | 137 | public override DrawGeometrySerializerBase ToSerializer() 138 | { 139 | var serializer = new DrawRectangleSerializer 140 | { 141 | Color = ((SolidColorBrush)pen.Brush).Color, 142 | StrokeThickness = pen.Thickness, 143 | Geometry = geometry.ToString() 144 | }; 145 | 146 | if (geometry.Transform != null) 147 | serializer.Matrix = geometry.Transform.Value; 148 | 149 | return serializer; 150 | } 151 | 152 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 153 | { 154 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 155 | 156 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 157 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 158 | 159 | this.IsFinish = true; 160 | 161 | this.Draw(); 162 | } 163 | 164 | #endregion 165 | 166 | #region 字段 167 | 168 | private Point? topLeft, bottomRight; 169 | 170 | #endregion 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Serialize/DrawAngleSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawAngleSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new AngleDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawAreaSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawAreaSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new AreaDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawArrowSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawArrowSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new ArrowDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawClosedCurveSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawClosedCurveSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new ClosedCurveDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawCurveSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawCurveSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new CurveDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawEllipseSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawEllipseSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new EllipseDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawGeometrySerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Xml.Serialization; 2 | 3 | namespace DrawTools.Serialize 4 | { 5 | [XmlRoot("Geometries")] 6 | public sealed class DrawGeometrySerializer 7 | { 8 | #region 属性 9 | 10 | [XmlArrayItem(typeof(DrawPenSerializer)), 11 | XmlArrayItem(typeof(DrawRangingSerializer)), 12 | XmlArrayItem(typeof(DrawLineSerializer)), 13 | XmlArrayItem(typeof(DrawArrowSerializer)), 14 | XmlArrayItem(typeof(DrawRectangleSerializer)), 15 | XmlArrayItem(typeof(DrawEllipseSerializer)), 16 | XmlArrayItem(typeof(DrawAngleSerializer)), 17 | XmlArrayItem(typeof(DrawPolylineSerializer)), 18 | XmlArrayItem(typeof(DrawCurveSerializer)), 19 | XmlArrayItem(typeof(DrawPolygonSerializer)), 20 | XmlArrayItem(typeof(DrawClosedCurveSerializer)), 21 | XmlArrayItem(typeof(DrawAreaSerializer)), 22 | XmlArrayItem(typeof(DrawTextSerializer))] 23 | public DrawGeometrySerializerBase[] Geometries { get; set; } 24 | 25 | #endregion 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Serialize/DrawGeometrySerializerBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Media; 3 | 4 | namespace DrawTools.Serialize 5 | { 6 | /// 7 | /// 画图几何图形序列化基类 8 | /// 9 | public abstract class DrawGeometrySerializerBase 10 | { 11 | /// 12 | /// 反序列化 13 | /// 14 | /// 15 | public abstract DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas); 16 | 17 | #region 属性 18 | 19 | public Double StrokeThickness { get; set; } 20 | 21 | public Color Color { get; set; } 22 | 23 | public String Geometry { get; set; } 24 | 25 | public Matrix Matrix { get; set; } 26 | 27 | #endregion 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Serialize/DrawLineSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace DrawTools.Serialize 4 | { 5 | public sealed class DrawLineSerializer : DrawGeometrySerializerBase 6 | { 7 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 8 | { 9 | var draw = new LineDrawTool(drawingCanvas); 10 | draw.DeserializeFrom(this); 11 | return draw; 12 | } 13 | 14 | #region 属性 15 | 16 | public Point StartPoint { get; set; } 17 | public Point EndPoint { get; set; } 18 | 19 | #endregion 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Serialize/DrawPenSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawPenSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new PenDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawPolygonSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawPolygonSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new PolygonDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawPolylineSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawPolylineSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new PolylineDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawRangingSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawRangingSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new RangingDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawRectangleSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace DrawTools.Serialize 2 | { 3 | public sealed class DrawRectangleSerializer : DrawGeometrySerializerBase 4 | { 5 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 6 | { 7 | var draw = new RectangleDrawTool(drawingCanvas); 8 | draw.DeserializeFrom(this); 9 | return draw; 10 | } 11 | 12 | #region 属性 13 | 14 | 15 | 16 | #endregion 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Serialize/DrawTextSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | 4 | namespace DrawTools.Serialize 5 | { 6 | public sealed class DrawTextSerializer : DrawGeometrySerializerBase 7 | { 8 | public override DrawGeometryBase Deserialize(DrawingCanvas drawingCanvas) 9 | { 10 | var draw = new TextDrawTool(drawingCanvas); 11 | draw.DeserializeFrom(this); 12 | return draw; 13 | } 14 | 15 | #region 属性 16 | 17 | public Point StartPoint { get; set; } 18 | 19 | public String Text { get; set; } 20 | 21 | public Double Width { get; set; } 22 | 23 | public Double Height { get; set; } 24 | 25 | #endregion 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TextDrawTool.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Serialize; 2 | using System; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools 8 | { 9 | /// 10 | /// 文本 11 | /// 12 | public sealed class TextDrawTool : DrawGeometryBase 13 | { 14 | public TextDrawTool(DrawingCanvas drawingCanvas) : base(drawingCanvas) 15 | { 16 | this.DrawingToolType = DrawToolType.Text; 17 | 18 | // 准备要处理的事件 19 | this.CanTouchDown = true; 20 | } 21 | 22 | #region 鼠标键盘事件 23 | 24 | public override Boolean OnTouchDown(Int32 touchId, Point point) 25 | { 26 | this.TouchId = touchId; 27 | 28 | if (!IsFinish && this.drawingCanvas.GetVisual(point) is TextDrawTool textDrawTool) 29 | { 30 | textDrawTool.TouchId = TouchId; 31 | textDrawTool.Edit(); 32 | 33 | this.IsFinish = true; 34 | this.CanTouchDown = false; 35 | } 36 | else if (textBox == null) 37 | { 38 | this.drawingCanvas.AddWorkingDrawTool(this); 39 | this.drawingCanvas.AddVisual(this); 40 | 41 | this.startPoint = point; 42 | this.pen = new Pen(this.drawingCanvas.Brush, 1); 43 | 44 | this.fontSize = this.drawingCanvas.FontSize; 45 | this.typeface = new Typeface(new FontFamily("Microsoft YaHei UI,Tahoma"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); 46 | 47 | this.padding = new Thickness(pen.Thickness * 3); 48 | this.minWidth = this.minHeight = fontSize * 2 + pen.Thickness * 8; 49 | 50 | AddTextBox(); 51 | } 52 | else 53 | OnTextBoxLostFocus(null, null); 54 | 55 | return true; 56 | } 57 | 58 | #endregion 59 | 60 | #region 绘图事件 61 | 62 | public override void Draw() 63 | { 64 | var dc = this.RenderOpen(); 65 | dc.DrawRectangle(Brushes.Transparent, null, selectRect); 66 | dc.DrawGeometry(pen.Brush, null, geometry); 67 | dc.Close(); 68 | } 69 | 70 | public override Boolean Select(Point point) 71 | { 72 | return selectRect.Contains(point); 73 | } 74 | 75 | public override Boolean Select(Geometry select) 76 | { 77 | var rect = select.GetRenderBounds(this.drawingCanvas.SelectPen); 78 | return selectRect.IntersectsWith(rect); 79 | } 80 | 81 | protected override Rect GetRenderBounds() 82 | { 83 | return selectRect; 84 | } 85 | 86 | public override void Move(Double dx, Double dy) 87 | { 88 | startPoint.X += dx; 89 | startPoint.Y += dy; 90 | 91 | selectRect.Location = startPoint; 92 | 93 | base.Move(dx, dy); 94 | } 95 | 96 | public override void Edit() 97 | { 98 | if (Mode == 2) 99 | return; 100 | 101 | Mode = 2; 102 | 103 | this.drawingCanvas.AddWorkingDrawTool(this); 104 | 105 | this.IsFinish = false; 106 | this.CanTouchDown = true; 107 | this.CanEdit = false; 108 | 109 | AddTextBox(); 110 | } 111 | 112 | #endregion 113 | 114 | #region 序列化 115 | 116 | public override DrawGeometrySerializerBase ToSerializer() 117 | { 118 | var serializer = new DrawTextSerializer 119 | { 120 | Color = ((SolidColorBrush)pen.Brush).Color, 121 | StrokeThickness = pen.Thickness, 122 | Geometry = geometry.ToString(), 123 | StartPoint = startPoint, 124 | Text = text, 125 | Width = selectRect.Width, 126 | Height = selectRect.Height 127 | }; 128 | 129 | if (geometry.Transform != null) 130 | serializer.Matrix = geometry.Transform.Value; 131 | 132 | return serializer; 133 | } 134 | 135 | public override void DeserializeFrom(DrawGeometrySerializerBase serializer) 136 | { 137 | this.pen = new Pen(new SolidColorBrush(serializer.Color), serializer.StrokeThickness); 138 | 139 | this.geometry = Geometry.Parse(serializer.Geometry).GetFlattenedPathGeometry(); 140 | this.geometry.Transform = new TranslateTransform(serializer.Matrix.OffsetX, serializer.Matrix.OffsetY); 141 | 142 | var textSerializer = (DrawTextSerializer)serializer; 143 | 144 | this.selectRect.Location = this.startPoint = textSerializer.StartPoint; 145 | this.text = textSerializer.Text; 146 | this.selectRect.Width = textSerializer.Width; 147 | this.selectRect.Height = textSerializer.Height; 148 | 149 | this.IsFinish = true; 150 | 151 | this.Draw(); 152 | } 153 | 154 | #endregion 155 | 156 | #region 私有方法 157 | 158 | private void AddTextBox() 159 | { 160 | this.textBox = new TextBox 161 | { 162 | Background = Brushes.Transparent, 163 | Foreground = pen.Brush, 164 | BorderThickness = new Thickness(), 165 | Padding = padding, 166 | FontFamily = new FontFamily("Microsoft YaHei UI,Tahoma"), 167 | FontSize = fontSize, 168 | Style = null, 169 | FocusVisualStyle = null, 170 | AcceptsReturn = true, 171 | AcceptsTab = true, 172 | MinWidth = minWidth, 173 | MinHeight = minHeight, 174 | Text = text 175 | }; 176 | 177 | textBox.SelectionStart = textBox.Text.Length; 178 | 179 | Canvas.SetLeft(textBox, startPoint.X + pen.Thickness); 180 | Canvas.SetTop(textBox, startPoint.Y + pen.Thickness); 181 | 182 | textBox.Loaded += OnTextBoxLoaded; 183 | textBox.SizeChanged += OnTextBoxSizeChanged; 184 | textBox.LostFocus += OnTextBoxLostFocus; 185 | 186 | this.drawingCanvas.Children.Add(textBox); 187 | } 188 | 189 | private void OnTextBoxLoaded(Object sender, RoutedEventArgs e) 190 | { 191 | textBox.Focus(); 192 | 193 | var ch = (ContentControl)textBox.Template.FindName("PART_ContentHost", textBox); 194 | 195 | var tf = ((Visual)ch.Content).TransformToAncestor(textBox); 196 | var offset = tf.Transform(new Point()); 197 | 198 | textPoint.X = startPoint.X + offset.X + pen.Thickness; 199 | textPoint.Y = startPoint.Y + offset.Y + pen.Thickness; 200 | } 201 | 202 | private void OnTextBoxSizeChanged(Object sender, SizeChangedEventArgs e) 203 | { 204 | actualWidth = e.NewSize.Width; 205 | actualHeight = e.NewSize.Height; 206 | 207 | selectRect = new Rect(startPoint, new Size(actualWidth + pen.Thickness * 2, actualHeight + pen.Thickness * 2)); 208 | 209 | var dc = this.RenderOpen(); 210 | dc.DrawRectangle(Brushes.Transparent, pen, selectRect); 211 | dc.Close(); 212 | } 213 | 214 | private void OnTextBoxLostFocus(Object sender, RoutedEventArgs e) 215 | { 216 | textBox.Loaded -= OnTextBoxLoaded; 217 | textBox.SizeChanged -= OnTextBoxSizeChanged; 218 | textBox.LostFocus -= OnTextBoxLostFocus; 219 | 220 | this.drawingCanvas.Focus(); 221 | this.drawingCanvas.Children.Remove(textBox); 222 | 223 | if (String.IsNullOrWhiteSpace(textBox.Text)) 224 | this.drawingCanvas.DeleteVisual(this); 225 | else 226 | { 227 | text = textBox.Text; 228 | 229 | var formattedText = new FormattedText(text, 230 | System.Globalization.CultureInfo.InvariantCulture, 231 | FlowDirection.LeftToRight, 232 | typeface, 233 | this.fontSize, 234 | pen.Brush); 235 | 236 | geometry = formattedText.BuildGeometry(textPoint).GetFlattenedPathGeometry(); 237 | Draw(); 238 | } 239 | 240 | this.drawingCanvas.DeleteWorkingDrawTool(this); 241 | 242 | this.IsFinish = true; 243 | this.CanTouchDown = false; 244 | this.CanEdit = true; 245 | 246 | textBox = null; 247 | } 248 | 249 | #endregion 250 | 251 | #region 字段 252 | 253 | private Double fontSize; 254 | private Typeface typeface; 255 | private Thickness padding; 256 | private TextBox textBox; 257 | private String text; 258 | private Point startPoint, textPoint; 259 | private Double minWidth, minHeight, actualWidth, actualHeight; 260 | 261 | #endregion 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /Themes/generic.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 33 | 34 | -------------------------------------------------------------------------------- /Utils/DpiHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Runtime.InteropServices; 4 | using System.Windows; 5 | using System.Windows.Media; 6 | 7 | namespace DrawTools.Utils 8 | { 9 | public struct Dpi 10 | { 11 | public Dpi(Double x, Double y) 12 | { 13 | DpiX = x; 14 | DpiY = y; 15 | 16 | Px2WpfX = 96 / DpiX; 17 | Px2WpfY = 96 / DpiY; 18 | } 19 | 20 | public Double DpiX { get; } 21 | 22 | public Double DpiY { get; } 23 | 24 | public Double Px2WpfX { get; } 25 | 26 | public Double Px2WpfY { get; } 27 | 28 | /// 29 | /// 英寸-厘米 30 | /// 31 | public static readonly Double In2Cm = 2.54; 32 | /// 33 | /// 英寸-磅 34 | /// 35 | public static readonly Double In2Pt = 72; 36 | /// 37 | /// 厘米-wpf 38 | /// 39 | public static readonly Double Cm2Wpf = 96 / 2.54; 40 | } 41 | 42 | public sealed class DpiHelper 43 | { 44 | #region Graphics 45 | 46 | public static Dpi GetDpiByGraphics(IntPtr hWnd) 47 | { 48 | using (var graphics = Graphics.FromHwnd(hWnd)) 49 | { 50 | return new Dpi(graphics.DpiX, graphics.DpiY); 51 | } 52 | } 53 | 54 | #endregion 55 | 56 | #region CompositionTarget 57 | 58 | public static Dpi GetDpiFromVisual(Visual visual) 59 | { 60 | var source = PresentationSource.FromVisual(visual); 61 | return (source == null || source.CompositionTarget == null) ? GetDpiByWin32(IntPtr.Zero) : new Dpi(96.0 * source.CompositionTarget.TransformToDevice.M11, 96.0 * source.CompositionTarget.TransformToDevice.M22); 62 | } 63 | 64 | #endregion 65 | 66 | #region Win32 API 67 | 68 | private const Int32 LOGPIXELSX = 88; 69 | private const Int32 LOGPIXELSY = 90; 70 | 71 | [DllImport("gdi32.dll")] 72 | private static extern Int32 GetDeviceCaps(IntPtr hdc, Int32 index); 73 | 74 | [DllImport("user32.dll")] 75 | private static extern IntPtr GetDC(IntPtr hWnd); 76 | 77 | [DllImport("user32.dll")] 78 | private static extern Int32 ReleaseDC(IntPtr hWnd, IntPtr hDc); 79 | 80 | public static Dpi GetDpiByWin32(IntPtr hwnd) 81 | { 82 | var hDc = GetDC(hwnd); 83 | 84 | var dpiX = GetDeviceCaps(hDc, LOGPIXELSX); 85 | var dpiY = GetDeviceCaps(hDc, LOGPIXELSY); 86 | 87 | ReleaseDC(hwnd, hDc); 88 | return new Dpi(dpiX, dpiY); 89 | } 90 | 91 | #endregion 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Utils/ImageHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Windows.Media.Imaging; 4 | 5 | namespace DrawTools.Utils 6 | { 7 | public static class ImageHelper 8 | { 9 | public static void Save(String filepath, params BitmapFrame[] frames) 10 | { 11 | BitmapEncoder encoder = null; 12 | 13 | 14 | switch (Path.GetExtension(filepath)) 15 | { 16 | case ".jpg": 17 | case ".jpeg": 18 | encoder = new JpegBitmapEncoder(); 19 | break; 20 | case ".png": 21 | encoder = new PngBitmapEncoder(); 22 | break; 23 | default: 24 | encoder = new BmpBitmapEncoder(); 25 | break; 26 | } 27 | 28 | 29 | foreach (var frame in frames) 30 | { 31 | encoder.Frames.Add(frame); 32 | } 33 | 34 | using (var fs = new FileStream(filepath, FileMode.Create)) 35 | { 36 | encoder.Save(fs); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Utils/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace DrawTools.Utils 5 | { 6 | public static class NativeMethods 7 | { 8 | /// 9 | /// 调用win api将指定名称的打印机设置为默认打印机 10 | /// 11 | /// 打印机名称 12 | /// 13 | [DllImport("winspool.drv")] 14 | public static extern Boolean SetDefaultPrinter(String Name); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Utils/TreeHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows; 3 | using System.Windows.Media; 4 | 5 | namespace DrawTools.Utils 6 | { 7 | public static class TreeHelper 8 | { 9 | /// 10 | /// 根据类型,查找父节点 11 | /// 12 | /// 查找对象类型 13 | /// 查找源点 14 | /// 筛选条件 15 | /// 查找对象 16 | public static T FindParent(this DependencyObject d, Func filter = null) where T : class 17 | { 18 | if (d == null) 19 | return null; 20 | 21 | var parent = LogicalTreeHelper.GetParent(d) ?? VisualTreeHelper.GetParent(d); 22 | 23 | while (parent != null) 24 | { 25 | if (parent is T t) 26 | { 27 | if (filter == null) 28 | return t; 29 | else if (filter.Invoke(t)) 30 | return t; 31 | } 32 | 33 | parent = LogicalTreeHelper.GetParent(parent) ?? VisualTreeHelper.GetParent(parent); 34 | } 35 | 36 | return null; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Views/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  11 | 12 | 23 | 46 | 47 | 70 | 71 | 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 | -------------------------------------------------------------------------------- /Views/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using DrawTools.Utils; 2 | using Microsoft.Win32; 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | 9 | namespace DrawTools.Views 10 | { 11 | /// 12 | /// MainWindow.xaml 的交互逻辑 13 | /// 14 | public partial class MainWindow 15 | { 16 | public MainWindow() 17 | { 18 | InitializeComponent(); 19 | 20 | color_picker.SelectedColorChanged += delegate { this.drawCanvas.Brush = color_picker.SelectedBrush; btn_color.IsChecked = false; }; 21 | color_picker.Canceled += delegate { btn_color.IsChecked = false; }; 22 | 23 | this.toolbar.AddHandler(RadioButton.CheckedEvent, new RoutedEventHandler(OnDrawToolChecked)); 24 | } 25 | 26 | private void OnDrawToolChecked(Object sender, RoutedEventArgs e) 27 | { 28 | if (e.Source is RadioButton btn && btn.Tag is String typeStr) 29 | drawCanvas.DrawingToolType = (DrawToolType)Enum.Parse(typeof(DrawToolType), typeStr); 30 | } 31 | 32 | private void btn_clear_Click(object sender, RoutedEventArgs e) 33 | { 34 | drawCanvas.Clear(); 35 | } 36 | 37 | private void OnSaveClick(Object sender, RoutedEventArgs e) 38 | { 39 | if (this.drawCanvas.GetDrawGeometries().Count() == 0) 40 | return; 41 | 42 | var folder = Path.Combine(Environment.CurrentDirectory, "Draws"); 43 | 44 | if (!Directory.Exists(folder)) 45 | Directory.CreateDirectory("Draws"); 46 | 47 | var dlg = new SaveFileDialog 48 | { 49 | Filter = "XML files (*.xml)|*.xml", 50 | OverwritePrompt = true, 51 | DefaultExt = "xml", 52 | InitialDirectory = folder, 53 | RestoreDirectory = true 54 | }; 55 | 56 | if ((Boolean)dlg.ShowDialog()) 57 | this.drawCanvas.Save(dlg.FileName); 58 | } 59 | 60 | private void OnOpenClick_1(Object sender, RoutedEventArgs e) 61 | { 62 | var folder = Path.Combine(Environment.CurrentDirectory, "Draws"); 63 | 64 | if (!Directory.Exists(folder)) 65 | Directory.CreateDirectory("Draws"); 66 | 67 | var dlg = new OpenFileDialog 68 | { 69 | Filter = "XML files (*.xml)|*.xml", 70 | DefaultExt = "xml", 71 | InitialDirectory = folder, 72 | RestoreDirectory = true 73 | }; 74 | 75 | if ((Boolean)dlg.ShowDialog()) 76 | this.drawCanvas.Load(dlg.FileName); 77 | } 78 | 79 | private void OnPrintClick(Object sender, RoutedEventArgs e) 80 | { 81 | var backgroundImage = this.drawViewer.BackgroundImage; 82 | 83 | this.drawCanvas.Print(backgroundImage.PixelWidth, backgroundImage.PixelHeight, DpiHelper.GetDpiFromVisual(this.drawCanvas), backgroundImage); 84 | } 85 | 86 | private void OnSaveImageClick(Object sender, RoutedEventArgs e) 87 | { 88 | var backgroundImage = this.drawViewer.BackgroundImage; 89 | 90 | var frame = this.drawCanvas.ToBitmapFrame(backgroundImage.PixelWidth, backgroundImage.PixelHeight, DpiHelper.GetDpiFromVisual(this.drawCanvas), backgroundImage); 91 | 92 | if (frame == null) 93 | return; 94 | 95 | var folder = Path.Combine(Environment.CurrentDirectory, "Images"); 96 | 97 | if (!Directory.Exists(folder)) 98 | Directory.CreateDirectory("Images"); 99 | 100 | var dlg = new SaveFileDialog 101 | { 102 | Filter = "Images files (*.jpg;*.jpeg;*.png;*.bmp)|*.jpg;*.jpeg;*.png;*.bmp", 103 | OverwritePrompt = true, 104 | DefaultExt = "jpg", 105 | InitialDirectory = folder, 106 | RestoreDirectory = true 107 | }; 108 | 109 | if ((Boolean)dlg.ShowDialog()) 110 | ImageHelper.Save(dlg.FileName, frame); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------