V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yanjinhua
V2EX  ›  C#

WPF 实现用户头像选择器

  •  
  •   yanjinhua · 2022-07-26 11:25:50 +08:00 · 1201 次点击
    这是一个创建于 611 天前的主题,其中的信息可能已经有所发展或是发生改变。

    制作一个用户头像选择器仿 WeGame

    • 制作一个用户头像选择Canvas为父控件所实现,展示图片使用ImagePath当作上方的蒙版;
    • Canvas:主要用途方便移动Image,设置ClipToBounds="True"裁剪为一个正方形200x200做为主要展示区域;
    • Image:展示需要裁剪的图片;

    • 当选择一个本地图片的时候判断宽与高谁更大,谁小就将它更改为200 ,另一边做等比缩放后给到DrawingVisual绘制一个新的BitmapFrameImage控件做展示;
    • 当移动图片的时候右侧展示当前区域使用CroppedBitmap进行裁剪并显示;
    • 源码Github Gitee

    1 )CropAvatar.xaml 代码如下;

    <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:controls="clr-namespace:WPFDevelopers.Controls">
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Basic/ControlBasic.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    
        <Style TargetType="controls:CropAvatar" BasedOn="{StaticResource ControlBasicStyle}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type controls:CropAvatar}">
                        <Canvas x:Name="PART_Canvas" ClipToBounds="True">
                            <Image x:Name="PART_Image" Cursor="SizeAll" ></Image>
                            <Path x:Name="PART_Layout" 
                                  Fill="{DynamicResource BlackSolidColorBrush}" 
                                  Width="200" Height="200" 
                                  Opacity=".5">
                                <Path.Data>
                                    <CombinedGeometry GeometryCombineMode="Xor">
                                        <CombinedGeometry.Geometry1>
                                            <RectangleGeometry Rect="0,0,200,200"/>
                                        </CombinedGeometry.Geometry1>
                                        <CombinedGeometry.Geometry2>
                                            <EllipseGeometry Center="100,100" RadiusX="100" RadiusY="100"/>
                                        </CombinedGeometry.Geometry2>
                                    </CombinedGeometry>
                                </Path.Data>
                            </Path>
                            <Grid x:Name="PART_Grid" Width="200" Height="200">
                                <Button x:Name="PART_ReplaceButton" Style="{StaticResource PathButton}"
                                        HorizontalAlignment="Right"
                                        VerticalAlignment="Top"
                                        Width="40" Height="40" ToolTip="更换图片"
                                        Visibility="Collapsed">
                                    <Button.Content>
                                        <Path Data="{StaticResource PathReplace}"
                                              Fill="{StaticResource PrimaryNormalSolidColorBrush}"
                                              Height="15"
                                              Width="15"
                                              Stretch="Fill" />
                                    </Button.Content>
                                </Button>
                                <Button x:Name="PART_AddButton" Style="{StaticResource PathButton}"
                                        Width="40" Height="40" ToolTip="选择图片">
                                    <Button.Content>
                                        <Path Data="{StaticResource PathAdd}"
                                              Fill="{StaticResource PrimaryNormalSolidColorBrush}"
                                              Height="20"
                                              Width="20"
                                              Stretch="Fill" 
                                              RenderTransformOrigin="0.5,0.5" IsHitTestVisible="False">
                                            <Path.RenderTransform>
                                                <RotateTransform Angle="45"/>
                                            </Path.RenderTransform>
                                        </Path>
                                    </Button.Content>
                                </Button>
                            </Grid>
                        </Canvas>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    
    </ResourceDictionary>
    

    2 )CropAvatar.cs 代码如下;

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
    using WPFDevelopers.Helpers;
    
    namespace WPFDevelopers.Controls
    {
        [TemplatePart(Name = CanvasTemplateName, Type = typeof(Canvas))]
        [TemplatePart(Name = ImageTemplateName, Type = typeof(Image))]
        [TemplatePart(Name = PathTemplateName, Type = typeof(Path))]
        [TemplatePart(Name = GridTemplateName, Type = typeof(Grid))]
        [TemplatePart(Name = ReplaceButtonTemplateName, Type = typeof(Button))]
        [TemplatePart(Name = AddButtonTemplateName, Type = typeof(Button))]
        public partial class CropAvatar : Control
        {
            private const string CanvasTemplateName = "PART_Canvas";
            private const string ImageTemplateName = "PART_Image";
            private const string PathTemplateName = "PART_Layout";
            private const string GridTemplateName = "PART_Grid";
            private const string ReplaceButtonTemplateName = "PART_ReplaceButton";
            private const string AddButtonTemplateName = "PART_AddButton";
            private Point point;
            private const int _size = 200;
            private bool isDown;
            private bool isLeft;
            private CroppedBitmap crop;
            private Canvas canvas;
            private Image image;
            private Path path;
            private Grid grid;
            private Button replaceButton, addButton;
            private int initialX, initialY, voffsetX, voffsetY;
            private double vNewStartX, vNewStartY, _StartX, _StartY, centerX, centerY;
            private BitmapFrame bitmapFrame;
    
            public ImageSource OutImageSource
            {
                get { return (ImageSource)GetValue(OutImageSourceProperty); }
                set { SetValue(OutImageSourceProperty, value); }
            }
    
            public static readonly DependencyProperty OutImageSourceProperty =
                DependencyProperty.Register("OutImageSource", typeof(ImageSource), typeof(CropAvatar), new PropertyMetadata(null));
    
    
            static CropAvatar()
            {
                DefaultStyleKeyProperty.OverrideMetadata(typeof(CropAvatar), new FrameworkPropertyMetadata(typeof(CropAvatar)));
            }
            public override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
                canvas = GetTemplateChild(CanvasTemplateName) as Canvas;
                canvas.Loaded += Canvas_Loaded;
                grid = GetTemplateChild(GridTemplateName) as Grid;
                image = GetTemplateChild(ImageTemplateName) as Image;
                image.MouseDown += Image_MouseDown;
                image.MouseMove += Image_MouseMove;
                image.MouseUp += Image_MouseUp;
                image.MouseLeave += Image_MouseLeave;
                path = GetTemplateChild(PathTemplateName) as Path;
                replaceButton = GetTemplateChild(ReplaceButtonTemplateName) as Button;
                replaceButton.Click += ReplaceButton_Click;
                addButton = GetTemplateChild(AddButtonTemplateName) as Button;
                addButton.Click += AddButton_Click;
            }
    
            private void Canvas_Loaded(object sender, RoutedEventArgs e)
            {
                if (sender is Canvas canvas)
                {
                    var width = canvas.ActualWidth;
                    var height = canvas.ActualHeight;
                    centerX = (width - path.Width) / 2.0d;
                    centerY = (height - path.Height) / 2.0d;
                    canvas.Clip = new RectangleGeometry(new Rect(centerX, centerY, 200, 200)); 
                    Canvas.SetLeft(path, centerX);
                    Canvas.SetTop(path, centerY);
                    Canvas.SetLeft(grid, centerX);
                    Canvas.SetTop(grid, centerY);
                }
            }
    
            private void Image_MouseLeave(object sender, MouseEventArgs e)
            {
                isDown = false;
                if (isLeft)
                    _StartX = Canvas.GetLeft(image);
                else
                    _StartY = Canvas.GetTop(image);
            }
    
            private void Image_MouseUp(object sender, MouseButtonEventArgs e)
            {
                if (isDown)
                {
                    var vPoint = e.GetPosition(this);
                    if (isLeft)
                    {
                        _StartX = Canvas.GetLeft(image);
                        initialX = voffsetX;
                    }
    
                    else
                    {
                        _StartY = Canvas.GetTop(image);
                        initialY = voffsetY;
                    }
                }
            }
    
            private void Image_MouseMove(object sender, MouseEventArgs e)
            {
                if (e.LeftButton == MouseButtonState.Pressed && isDown)
                {
                    var vPoint = e.GetPosition(this);
                    if (isLeft)
                    {
                        var voffset = vPoint.X - point.X;
                        vNewStartX = _StartX + voffset;
                        var xPath = Canvas.GetLeft(path);
                        if (vNewStartX <= xPath && vNewStartX >= -(bitmapFrame.Width - 200 - xPath))
                        {
                            Canvas.SetLeft(image, vNewStartX);
                            voffsetX = initialX - (int)voffset;
                            voffsetX = voffsetX < 0 ? 0 : voffsetX;
                            crop = new CroppedBitmap(bitmapFrame, new Int32Rect(voffsetX, 0, _size, _size));
    
                        }
                    }
                    else
                    {
                        var voffset = vPoint.Y - point.Y;
                        vNewStartY = _StartY + voffset;
                        var yPath = Canvas.GetTop(path);
                        if (vNewStartY <= yPath && vNewStartY >= -(bitmapFrame.Height - 200 - yPath))
                        {
                            Canvas.SetTop(image, vNewStartY);
                            voffsetY = initialY - (int)voffset;
                            voffsetY = voffsetY < 0 ? 0 : voffsetY;
                            crop = new CroppedBitmap(bitmapFrame, new Int32Rect(0, voffsetY, _size, _size));
                        }
                    }
                    OutImageSource = crop;
                }
            }
    
            private void Image_MouseDown(object sender, MouseButtonEventArgs e)
            {
                isDown = true;
                point = e.GetPosition(this);
            }
    
            private void ReplaceButton_Click(object sender, RoutedEventArgs e)
            {
                InitialImage();
            }
    
            private void AddButton_Click(object sender, RoutedEventArgs e)
            {
                InitialImage();
            }
    
            void InitialImage()
            {
                vNewStartX = 0;
                vNewStartY = 0;
                var uri = ControlsHelper.ImageUri();
                if (uri == null) return;
                var bitmap = new BitmapImage(uri);
                if (bitmap.Height > bitmap.Width)
                {
                    double scale = (double)bitmap.Width / (double)path.Width;
                    image.Width = _size;
                    image.Height = (double)bitmap.Height / scale;
                    isLeft = false;
                }
                else if (bitmap.Width > bitmap.Height)
                {
                    double scale = (double)bitmap.Height / (double)path.Height;
                    image.Width = (double)bitmap.Width / scale;
                    image.Height = _size;
                    isLeft = true;
                }
                bitmapFrame = ControlsHelper.CreateResizedImage(bitmap, (int)image.Width, (int)image.Height, 0);
                image.Source = bitmapFrame;
                if (image.Source != null)
                {
                    replaceButton.Visibility = Visibility.Visible;
                    addButton.Visibility = Visibility.Collapsed;
                }
                Canvas.SetLeft(grid, centerX);
                Canvas.SetTop(grid, centerY);
                _StartX = (canvas.ActualWidth - image.Width) / 2.0d;
                _StartY = (canvas.ActualHeight - image.Height) / 2.0d;
                Canvas.SetLeft(image, _StartX);
                Canvas.SetTop(image, _StartY);        
                if (isLeft)
                {
                    initialX = (int)(image.Width - 200) / 2;
                    initialY = 0;
                    crop = new CroppedBitmap(bitmapFrame, new Int32Rect(initialX, 0, _size, _size));
    
                }
                else
                {
                    initialY = (int)(image.Height - 200) / 2;
                    initialX = 0;
                    crop = new CroppedBitmap(bitmapFrame, new Int32Rect(0, initialY, _size, _size));
                }
                OutImageSource = crop;
            }
           
        }
    }
    
    

    3 )CropAvatarWindow.xaml使用如下;

    <ws:Window x:Class="WPFDevelopers.Samples.ExampleViews.CropAvatarWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:wpfdev="https://github.com/WPFDevelopersOrg/WPFDevelopers"
            xmlns:ws="https://github.com/WPFDevelopersOrg.WPFDevelopers.Minimal"
            mc:Ignorable="d"  WindowStyle="ToolWindow" ResizeMode="NoResize"
            WindowStartupLocation="CenterScreen"
            Title="WPF 开发者-头像选择器" Height="450" Width="800">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <wpfdev:CropAvatar x:Name="MyCropAvatar"/>
            <Image Grid.Column="1" Name="CropAvatarImage" Source="{Binding ElementName=MyCropAvatar,Path=OutImageSource}" 
                   Stretch="Fill" Width="200" Height="200">
                <Image.Clip>
                    <EllipseGeometry Center="100,100" RadiusX="100" RadiusY="100"/>
                </Image.Clip>
            </Image>
            <UniformGrid Grid.Row="1" Grid.ColumnSpan="2" 
                         HorizontalAlignment="Center" 
                         VerticalAlignment="Center">
                <Button  Content="保存" Click="btnSave_Click" Style="{StaticResource PrimaryButton}" Margin="4,0"/>
                <Button  Content="关闭" Click="btnClose_Click" Margin="4,0"/>
            </UniformGrid>
        </Grid>
    </ws:Window>
    
    

    4 ) CropAvatarWindow.xaml.cs 代码如下;

    using System.Windows;
    
    namespace WPFDevelopers.Samples.ExampleViews
    {
        /// <summary>
        /// CropAvatarWindow.xaml 的交互逻辑
        /// </summary>
        public partial class CropAvatarWindow 
        {
            public CropAvatarWindow()
            {
                InitializeComponent();
            }
    
            private void btnSave_Click(object sender, RoutedEventArgs e)
            {
                DialogResult = true;
            }
    
            private void btnClose_Click(object sender, RoutedEventArgs e)
            {
                DialogResult = false;
            }
        }
    }
    
    

    5 ) CropAvatarExample.xaml 使用如下;

    <UserControl x:Class="WPFDevelopers.Samples.ExampleViews.CropAvatarExample"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:wpfdev="https://github.com/WPFDevelopersOrg/WPFDevelopers"
                 xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews"
                 mc:Ignorable="d" 
                 d:DesignHeight="450" d:DesignWidth="800">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Button Content="图像选择器" VerticalAlignment="Center" HorizontalAlignment="Center" Click="Button_Click"/>
            <Image Grid.Column="1" Name="MyImage"
                   Stretch="Fill" Width="200" Height="200">
                <Image.Clip>
                    <EllipseGeometry Center="100,100" RadiusX="100" RadiusY="100"/>
                </Image.Clip>
            </Image>
        </Grid>
    </UserControl>
    
    

    6 ) CropAvatarExample.xaml.cs 代码如下;

    using System.Windows.Controls;
    
    namespace WPFDevelopers.Samples.ExampleViews
    {
        /// <summary>
        /// CropAvatarExample.xaml 的交互逻辑
        /// </summary>
        public partial class CropAvatarExample : UserControl
        {
            public CropAvatarExample()
            {
                InitializeComponent();
            }
    
            private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
            {
                var cropAvatarWindow = new CropAvatarWindow();
                if (cropAvatarWindow.ShowDialog() == true)
                {
                    MyImage.Source = cropAvatarWindow.CropAvatarImage.Source;
                }
            }
        }
    }
    
    

    [1] Github: https://github.com/WPFDevelopersOrg/WPFDevelopers

    [2] Gitee: https://gitee.com/WPFDevelopersOrg/WPFDevelopers

    2 条回复    2022-07-26 13:13:21 +08:00
    Eiden
        1
    Eiden  
       2022-07-26 12:19:41 +08:00
    加个缩放就好了
    yanjinhua
        2
    yanjinhua  
    OP
       2022-07-26 13:13:21 +08:00
    @Eiden 感谢评论,目前没有缩放,以后增加。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3970 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 52ms · UTC 10:21 · PVG 18:21 · LAX 03:21 · JFK 06:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.