Windows 8 提供兩種手寫繪圖路徑:Pointer 事件手動繪製 (教學/輕量) 與 InkManager 完整管線 (生產級)。本文從零實作兩版塗鴉板,涵蓋貝塞爾曲線渲染、選取/移動/刪除、剪貼簿、儲存載入 GIF、中文手寫辨識。
Dictionary<uint, Point?> 追蹤多點觸控各指針上一座標。PointerMoved 計算兩點距離 > 4 像素時繪製 Line 直線段。PointerReleased/Exited 清除追蹤。void canvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var pt = e.GetCurrentPoint(canvas);
if (_dicPoint.ContainsKey(pt.PointerId) && _dicPoint[pt.PointerId].HasValue)
{
var curr = pt.Position;
var prev = _dicPoint[pt.PointerId].Value;
if (Distance(curr, prev) > 4)
{
canvas.Children.Add(new Line { X1=prev.X, Y1=prev.Y, X2=curr.X, Y2=curr.Y,
StrokeThickness=5, Stroke=Orange, StrokeEndLineCap=PenLineCap.Round });
_dicPoint[pt.PointerId] = curr;
}
}
}
InkStroke 轉三次貝塞爾曲線 Path,支援即時筆跡、選取高亮、完整重繪。public static Path InkStroke2BezierPath(InkStroke stroke)
{
var figure = new PathFigure();
var segs = stroke.GetRenderingSegments().GetEnumerator();
segs.MoveNext();
figure.StartPoint = segs.Current.Position;
while (segs.MoveNext())
{
figure.Segments.Add(new BezierSegment
{
Point1 = segs.Current.BezierControlPoint1,
Point2 = segs.Current.BezierControlPoint2,
Point3 = segs.Current.Position
});
}
return new Path
{
Data = new PathGeometry { Figures = { figure } },
Stroke = new SolidColorBrush(stroke.DrawingAttributes.Color),
StrokeThickness = stroke.DrawingAttributes.Size.Width,
StrokeLineJoin = PenLineJoin.Round,
StrokeStartLineCap = PenLineCap.Round
};
}
| 功能 | API / 實作要點 |
|---|---|
| 三種模式切換 | InkManager.Mode = Inking/Erasing/Selecting + RadioButton 綁定 |
| 即時筆跡渲染 | EnterLiveRendering/UpdateLiveRender/ExitLiveRendering 畫直線預覽,筆跡完成後換貝塞爾曲線 |
| 選取高亮 | UpdateSelectionRender 疊加白色較粗 Path 表示選取狀態 |
| 刪除選取/全部 | InkManager.DeleteSelected() / 遍歷 Selected=true 後刪除 |
| 移動選取筆跡 | InkManager.MoveSelected(new Point(50,5)) |
| 剪切/複製/貼上 | CopySelectedToClipboard/PasteFromClipboard/DeleteSelected |
| 儲存/載入 GIF | InkManager.SaveAsync/LoadAsync(stream) + FileSavePicker/FileOpenPicker |
| 中文手寫辨識 | RecognizeAsync(InkRecognitionTarget.All) + 設定 Microsoft 中文(簡體)手寫識別器 |
void canvas_PointerPressed(object sender, PointerRoutedEventArgs e)
{
_pointerId = (int)e.GetCurrentPoint(canvas).PointerId;
switch (_inkManager.Mode)
{
case Inking: _renderer.EnterLiveRendering(pt, _inkDrawingAttributes); break;
case Selecting: _renderer.EnterLiveRendering(pt, _inkDrawingAttributesForSelect); break;
}
_inkManager.ProcessPointerDown(pt);
}
void canvas_PointerMoved(object sender, PointerRoutedEventArgs e)
{
var pt = e.GetCurrentPoint(canvas);
if (_pointerId == (int)pt.PointerId)
{
if (_inkManager.Mode == Erasing)
{
var rect = _inkManager.ProcessPointerUpdate(pt);
if (rect.Width>0) { _renderer.Clear(); _renderer.UpdateInkRender(_inkManager.GetStrokes()); }
}
else // Inking/Selecting
{
foreach (var ipt in e.GetIntermediatePoints(canvas).Reverse())
_inkManager.ProcessPointerUpdate(ipt);
_renderer.UpdateLiveRender(pt);
}
}
}
void canvas_PointerReleased(object sender, PointerRoutedEventArgs e)
{
if (_pointerId == (int)e.GetCurrentPoint(canvas).PointerId)
{
_pointerId = -1;
_inkManager.ProcessPointerUp(e.GetCurrentPoint(canvas));
if (_inkManager.Mode == Inking)
_renderer.UpdateInkRender(_inkManager.GetStrokes().Last());
else if (_inkManager.Mode == Selecting)
_renderer.UpdateSelectionRender();
}
}
| 類別 | 核心屬性/方法 |
|---|---|
| InkManager | Mode, SetDefaultDrawingAttributes, ProcessPointerDown/Update/Up, GetStrokes, SaveAsync/LoadAsync, RecognizeAsync, CopySelectedToClipboard, PasteFromClipboard, MoveSelected, DeleteSelected |
| InkDrawingAttributes | Color, Size, FitToCurve (貝塞爾/直線), PenTip (Circle/Rectangle), IgnorePressure |
| InkStroke | DrawingAttributes, BoundingRect, Selected, Recognized, Clone(), GetRenderingSegments() |
| InkStrokeRenderingSegment | BezierControlPoint1, BezierControlPoint2, Position |
簡易版適合理解 Pointer 事件機制;專業版 InkManager + InkRenderer 是生產環境標準架構,支援完整筆跡生命週期管理、跨裝置儲存格式 (GIF)、中文手寫辨識,可直接擴充為簽名板、批註工具、手寫記事本核心模組。