Skip to content

Commit 1312ed8

Browse files
committed
feat: add mosaic function
1 parent 94ce3f6 commit 1312ed8

2 files changed

Lines changed: 227 additions & 7 deletions

File tree

src/WPFDevelopers.Shared/Controls/ScreenCut/ScreenCut.cs

Lines changed: 212 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
using Brush = System.Windows.Media.Brush;
1717
using Brushes = System.Windows.Media.Brushes;
1818
using Button = System.Windows.Controls.Button;
19-
using Cursors = System.Windows.Input.Cursors;
19+
using Color = System.Windows.Media.Color;
2020
using Control = System.Windows.Controls.Control;
2121
using Cursors = System.Windows.Input.Cursors;
2222
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
2323
using MouseEventArgs = System.Windows.Input.MouseEventArgs;
2424
using Path = System.Windows.Shapes.Path;
2525
using Point = System.Windows.Point;
26-
using Brush = System.Windows.Media.Brush;
26+
using RadioButton = System.Windows.Controls.RadioButton;
2727
using Rectangle = System.Windows.Shapes.Rectangle;
2828
using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
2929
using TextBox = System.Windows.Controls.TextBox;
@@ -68,6 +68,7 @@ public enum ScreenCutMouseType
6868
DrawArrow,
6969
DrawText,
7070
DrawInk,
71+
DrawMosaic
7172
}
7273
[TemplatePart(Name = CanvasTemplateName, Type = typeof(Canvas))]
7374
[TemplatePart(Name = LeftRectangleTemplateName, Type = typeof(Rectangle))]
@@ -83,6 +84,7 @@ public enum ScreenCutMouseType
8384
[TemplatePart(Name = EllipseRadioButtonTemplateName, Type = typeof(RadioButton))]
8485
[TemplatePart(Name = ArrowRadioButtonTemplateName, Type = typeof(RadioButton))]
8586
[TemplatePart(Name = InkRadioButtonTemplateName, Type = typeof(RadioButton))]
87+
[TemplatePart(Name = MosaicRadioButtonTemplateName, Type = typeof(RadioButton))]
8688
[TemplatePart(Name = TextRadioButtonTemplateName, Type = typeof(RadioButton))]
8789
[TemplatePart(Name = PopupTemplateName, Type = typeof(Popup))]
8890
[TemplatePart(Name = BorderPopupTemplateName, Type = typeof(Border))]
@@ -104,6 +106,7 @@ public class ScreenCut : Window, IDisposable
104106
private const string EllipseRadioButtonTemplateName = "PART_EllipseRadioButton";
105107
private const string ArrowRadioButtonTemplateName = "PART_ArrowRadioButton";
106108
private const string InkRadioButtonTemplateName = "PART_InkRadioButton";
109+
private const string MosaicRadioButtonTemplateName = "PART_MosaicRadioButton";
107110
private const string TextRadioButtonTemplateName = "PART_TextRadioButton";
108111
private const string PopupTemplateName = "PART_Popup";
109112
private const string BorderPopupTemplateName = "PART_BorderPopup";
@@ -126,6 +129,7 @@ public class ScreenCut : Window, IDisposable
126129
_radioButtonEllipse,
127130
_arrowRadioButton,
128131
_inkRadioButton,
132+
_mosaicRadioButton,
129133
_textRadioButton;
130134

131135
private Rectangle _leftRectangle, _topRectangle, _rightRectangle, _bottomRectangle;
@@ -192,6 +196,11 @@ public class ScreenCut : Window, IDisposable
192196
public static int CaptureScreenID = -1;
193197
private Bitmap _screenCapture;
194198
private ScreenDPI _screenDPI;
199+
private RenderTargetBitmap _imageSnapshot;
200+
private Path _currentStrokeContainer = null;
201+
private List<Rectangle> _currentStrokeRectangles = new List<Rectangle>();
202+
private Stack<UIElement> _strokeHistory = new Stack<UIElement>();
203+
195204
public ScreenCut(int index)
196205
{
197206
_screenIndex = index;
@@ -251,6 +260,9 @@ public override void OnApplyTemplate()
251260
_inkRadioButton = GetTemplateChild(InkRadioButtonTemplateName) as RadioButton;
252261
if (_inkRadioButton != null)
253262
_inkRadioButton.Click += RadioButtonInk_Click;
263+
_mosaicRadioButton = GetTemplateChild(MosaicRadioButtonTemplateName) as RadioButton;
264+
if (_mosaicRadioButton != null)
265+
_mosaicRadioButton.Click += RadioButtonMosaic_Click;
254266
_textRadioButton = GetTemplateChild(TextRadioButtonTemplateName) as RadioButton;
255267
if (_textRadioButton != null)
256268
_textRadioButton.Click += RadioButtonText_Click;
@@ -277,6 +289,7 @@ protected override void OnClosed(EventArgs e)
277289
private void ScreenCut_Loaded(object sender, RoutedEventArgs e)
278290
{
279291
_canvas.Background = new ImageBrush(ImagingHelper.CreateBitmapSourceFromBitmap(CopyScreen()));
292+
TakeSnapshot();
280293
}
281294

282295
private ScreenDPI GetScreenDPI(int screenIndex)
@@ -318,6 +331,11 @@ private void RadioButtonInk_Click(object sender, RoutedEventArgs e)
318331
RadioButtonChecked(_inkRadioButton, ScreenCutMouseType.DrawInk);
319332
}
320333

334+
private void RadioButtonMosaic_Click(object sender, RoutedEventArgs e)
335+
{
336+
RadioButtonChecked(_mosaicRadioButton, ScreenCutMouseType.DrawMosaic);
337+
}
338+
321339
private void RadioButtonText_Click(object sender, RoutedEventArgs e)
322340
{
323341
RadioButtonChecked(_textRadioButton, ScreenCutMouseType.DrawText);
@@ -355,6 +373,11 @@ private void RadioButtonChecked(RadioButton radioButton, ScreenCutMouseType scre
355373
_border.Cursor = Cursors.Arrow;
356374
if (_popup.PlacementTarget != null && _popup.IsOpen)
357375
_popup.IsOpen = false;
376+
if(screenCutMouseTypeRadio != ScreenCutMouseType.DrawMosaic)
377+
{
378+
_popup.PlacementTarget = radioButton;
379+
_popup.IsOpen = true;
380+
}
358381
DisposeControl();
359382
}
360383
else
@@ -474,6 +497,11 @@ protected override void OnPreviewKeyDown(KeyEventArgs e)
474497
}
475498
else if (e.KeyStates == Keyboard.GetKeyStates(Key.Z) && Keyboard.Modifiers == ModifierKeys.Control)
476499
{
500+
if (_screenCutMouseType == ScreenCutMouseType.DrawMosaic)
501+
{
502+
UndoLastStroke();
503+
return;
504+
}
477505
if (_canvas.Children.Count > 0)
478506
_canvas.Children.Remove(_canvas.Children[_canvas.Children.Count - 1]);
479507

@@ -504,6 +532,10 @@ protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
504532
_editBar.Visibility = Visibility.Hidden;
505533
_pointEnd = _pointStart;
506534
_rect = new Rect(_pointStart.Value, _pointEnd.Value);
535+
if (_screenCutMouseType == ScreenCutMouseType.DrawMosaic)
536+
{
537+
_currentStrokeRectangles.Clear();
538+
}
507539
}
508540
else
509541
{
@@ -639,6 +671,183 @@ protected override void OnPreviewMouseMove(MouseEventArgs e)
639671
case ScreenCutMouseType.DrawInk:
640672
DrwaInkControl(current);
641673
break;
674+
case ScreenCutMouseType.DrawMosaic:
675+
if ((current - _pointStart.Value).Length < 10)
676+
return;
677+
_pointStart = current;
678+
DrawMosaicBlock(current, 10, 20);
679+
break;
680+
}
681+
}
682+
}
683+
684+
private void TakeSnapshot()
685+
{
686+
_canvas.Measure(new System.Windows.Size(_canvas.ActualWidth, _canvas.ActualHeight));
687+
_canvas.Arrange(new Rect(0, 0, _canvas.ActualWidth, _canvas.ActualHeight));
688+
689+
_imageSnapshot = new RenderTargetBitmap(
690+
(int)_canvas.ActualWidth,
691+
(int)_canvas.ActualHeight,
692+
96, 96, PixelFormats.Pbgra32);
693+
694+
_imageSnapshot.Render(_canvas);
695+
}
696+
697+
private void DrawMosaicBlock(Point center, int blockSize, int brushSize)
698+
{
699+
if (_imageSnapshot == null) return;
700+
701+
int mosaicSize = blockSize;
702+
int blocksPerRow = brushSize / mosaicSize;
703+
704+
for (int i = 0; i < blocksPerRow; i++)
705+
{
706+
for (int j = 0; j < blocksPerRow; j++)
707+
{
708+
double x = center.X - brushSize / 2 + i * mosaicSize;
709+
double y = center.Y - brushSize / 2 + j * mosaicSize;
710+
711+
Point blockCenter = new Point(x + mosaicSize / 2, y + mosaicSize / 2);
712+
Color color = GetAreaAverageColor(blockCenter, mosaicSize);
713+
714+
var block = new Rectangle
715+
{
716+
Width = mosaicSize,
717+
Height = mosaicSize,
718+
Fill = new SolidColorBrush(color),
719+
IsHitTestVisible = false
720+
};
721+
722+
Canvas.SetLeft(block, x);
723+
Canvas.SetTop(block, y);
724+
725+
_canvas.Children.Add(block);
726+
727+
_currentStrokeRectangles.Add(block);
728+
}
729+
}
730+
}
731+
732+
private void CompleteCurrentStroke()
733+
{
734+
if (_currentStrokeRectangles.Count == 0) return;
735+
RemoveTemporaryRectangles();
736+
CreateStrokeContainer();
737+
_canvas.Children.Add(_currentStrokeContainer);
738+
_strokeHistory.Push(_currentStrokeContainer);
739+
_currentStrokeContainer = null;
740+
_currentStrokeRectangles.Clear();
741+
}
742+
743+
private void CreateStrokeContainer()
744+
{
745+
if (_currentStrokeRectangles.Count == 0) return;
746+
747+
double minX = double.MaxValue;
748+
double minY = double.MaxValue;
749+
double maxX = double.MinValue;
750+
double maxY = double.MinValue;
751+
752+
foreach (var rect in _currentStrokeRectangles)
753+
{
754+
double x = Canvas.GetLeft(rect);
755+
double y = Canvas.GetTop(rect);
756+
757+
minX = Math.Min(minX, x);
758+
minY = Math.Min(minY, y);
759+
maxX = Math.Max(maxX, x + rect.Width);
760+
maxY = Math.Max(maxY, y + rect.Height);
761+
}
762+
763+
double width = maxX - minX;
764+
double height = maxY - minY;
765+
766+
var roundedRect = CreateRoundedRectangleGeometry(width, height);
767+
768+
var container = new Path
769+
{
770+
Data = roundedRect,
771+
IsHitTestVisible = false,
772+
Fill = CreateMosaicVisualBrush(minX, minY, width, height)
773+
};
774+
775+
Canvas.SetLeft(container, minX);
776+
Canvas.SetTop(container, minY);
777+
778+
_currentStrokeContainer = container;
779+
}
780+
781+
private Geometry CreateRoundedRectangleGeometry(double width, double height)
782+
{
783+
bool isVertical = height > width * 1.5;
784+
double cornerRadius = isVertical ? Math.Min(width / 2, 30) : Math.Min(height / 2, 30);
785+
return new RectangleGeometry(new Rect(0, 0, width, height), cornerRadius, cornerRadius);
786+
}
787+
788+
private Brush CreateMosaicVisualBrush(double left, double top, double width, double height)
789+
{
790+
var drawingVisual = new DrawingVisual();
791+
792+
using (var context = drawingVisual.RenderOpen())
793+
{
794+
foreach (var rect in _currentStrokeRectangles)
795+
{
796+
double relativeX = Canvas.GetLeft(rect) - left;
797+
double relativeY = Canvas.GetTop(rect) - top;
798+
799+
var rectGeometry = new RectangleGeometry(
800+
new Rect(relativeX, relativeY, rect.Width, rect.Height));
801+
802+
context.DrawGeometry(rect.Fill, null, rectGeometry);
803+
}
804+
}
805+
return new VisualBrush(drawingVisual)
806+
{
807+
Stretch = Stretch.None,
808+
AlignmentX = AlignmentX.Left,
809+
AlignmentY = AlignmentY.Top
810+
};
811+
}
812+
813+
private void RemoveTemporaryRectangles()
814+
{
815+
foreach (var rect in _currentStrokeRectangles)
816+
{
817+
_canvas.Children.Remove(rect);
818+
}
819+
}
820+
821+
private Color GetAreaAverageColor(Point center, int areaSize)
822+
{
823+
try
824+
{
825+
double scaleX = _imageSnapshot.PixelWidth / _canvas.ActualWidth;
826+
double scaleY = _imageSnapshot.PixelHeight / _canvas.ActualHeight;
827+
int pixelX = (int)(center.X * scaleX);
828+
int pixelY = (int)(center.Y * scaleY);
829+
int halfSize = areaSize / 2;
830+
int totalR = 0, totalG = 0, totalB = 0;
831+
int count = 0;
832+
for (int dx = -halfSize; dx <= halfSize; dx++)
833+
{
834+
for (int dy = -halfSize; dy <= halfSize; dy++)
835+
{
836+
int x = pixelX + dx;
837+
int y = pixelY + dy;
838+
839+
if (x >= 0 && x < _imageSnapshot.PixelWidth &&
840+
y >= 0 && y < _imageSnapshot.PixelHeight)
841+
{
842+
byte[] pixels = new byte[4];
843+
_imageSnapshot.CopyPixels(new Int32Rect(x, y, 1, 1), pixels, 4, 0);
844+
845+
totalR += pixels[2];
846+
totalG += pixels[1];
847+
totalB += pixels[0];
848+
count++;
849+
}
850+
}
642851
}
643852
if (count == 0) return Colors.Gray;
644853
return Color.FromRgb(
@@ -1020,7 +1229,7 @@ protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
10201229
&&
10211230
_inkRadioButton.IsChecked != true
10221231
&&
1023-
_radioButtonInk.IsChecked != true)
1232+
_mosaicRadioButton.IsChecked != true)
10241233
_screenCutMouseType = ScreenCutMouseType.Default;
10251234
else
10261235
DisposeControl();

src/WPFDevelopers.Shared/Themes/ScreenCut.xaml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,26 +99,37 @@
9999
UseLayoutRounding="True" />
100100
</RadioButton>
101101
<RadioButton
102-
x:Name="PART_RadioButtonArrow"
102+
x:Name="PART_ArrowRadioButton"
103103
Margin="4,0"
104104
Style="{StaticResource WD.PathRadioButton}"
105105
ToolTip="{Binding [Arrow], Source={x:Static resx:LanguageManager.Instance}}">
106+
<controls:PathIcon
107+
Width="16"
108+
Height="19"
109+
Foreground="{DynamicResource WD.RegularTextBrush}"
110+
Kind="ArrowRightTop" />
111+
</RadioButton>
112+
<RadioButton
113+
x:Name="PART_InkRadioButton"
114+
Margin="4,0"
115+
Style="{StaticResource WD.PathRadioButton}"
116+
ToolTip="{Binding [Ink], Source={x:Static resx:LanguageManager.Instance}}">
106117
<controls:PathIcon
107118
Width="18"
108119
Height="18"
109120
Foreground="{DynamicResource WD.RegularTextBrush}"
110-
Kind="ArrowRightTop" />
121+
Kind="Pencil" />
111122
</RadioButton>
112123
<RadioButton
113-
x:Name="PART_RadioButtonInk"
124+
x:Name="PART_MosaicRadioButton"
114125
Margin="4,0"
115126
Style="{StaticResource WD.PathRadioButton}"
116127
ToolTip="{Binding [Ink], Source={x:Static resx:LanguageManager.Instance}}">
117128
<controls:PathIcon
118129
Width="18"
119130
Height="18"
120131
Foreground="{DynamicResource WD.RegularTextBrush}"
121-
Kind="Ink" />
132+
Kind="Mosaic" />
122133
</RadioButton>
123134
<RadioButton
124135
x:Name="PART_TextRadioButton"

0 commit comments

Comments
 (0)