prosource

WPF의 커스텀커서?

probook 2023. 4. 13. 20:55
반응형

WPF의 커스텀커서?

WPF 앱에서 이미지 또는 아이콘을 커스텀 커서로 사용하고 싶습니다.내가 어떻게 그럴 수 있을까?

두 가지 기본 옵션이 있습니다.

  1. 위에 .this.Cursor = Cursors.None;원하는 기술을 사용하여 자신만의 커서를 그릴 수 있습니다.그런 다음 마우스 이벤트에 응답하여 커서의 위치와 모양을 업데이트합니다.을 하다
  1. .cur 또는 .ani 파일에서 이미지를 로드하여 새 커서 개체를 만듭니다.이러한 종류의 파일은 Visual Studio에서 생성하고 편집할 수 있습니다.그것들을 다루기 위한 무료 유틸리티도 몇 개 떠돌고 있다.기본적으로 커서가 이미지의 어느 위치에 있는지를 나타내는 "핫 스폿"을 지정하는 이미지(또는 애니메이션 이미지)입니다.

절대 가 필요하므로 해 주십시오.Cursor(string fileName)컨스트럭터Lamely, 상대 경로 또는 Pack URI는 기능하지 않습니다.상대 경로 또는 어셈블리로 채워진 리소스에서 커서를 로드해야 할 경우 파일에서 스트림을 가져와Cursor(Stream cursorStream)증나짜

한편 XAML Atribut을 사용하여 커서를 로드할 때 상대 경로로 지정하면 효과가 있습니다.커서를 숨겨진 컨트롤에 로드한 후 참조를 복사하여 다른 컨트롤에서 사용할 수 있습니다.해 본 적은 없지만, 효과가 있을 거예요.

Peter가 언급했듯이, 이미 .cur 파일이 있는 경우 리소스 섹션에 더미 요소를 만든 후 필요할 때 더미의 커서를 참조하여 내장 리소스로 사용할 수 있습니다.

예를 들어, 선택한 도구에 따라 비표준 커서를 표시하려고 합니다.

리소스에 추가:

<Window.Resources>
    <ResourceDictionary>
        <TextBlock x:Key="CursorGrab" Cursor="Resources/Cursors/grab.cur"/>
        <TextBlock x:Key="CursorMagnify" Cursor="Resources/Cursors/magnify.cur"/>
    </ResourceDictionary>
</Window.Resources>

코드에서 참조되는 내장 커서의 예:

if (selectedTool == "Hand")
    myCanvas.Cursor = ((TextBlock)this.Resources["CursorGrab"]).Cursor;
else if (selectedTool == "Magnify")
    myCanvas.Cursor = ((TextBlock)this.Resources["CursorMagnify"]).Cursor;
else
    myCanvas.Cursor = Cursor.Arrow;

커서 디스플레이를 직접 관리하거나 Visual Studio를 사용하여 많은 사용자 지정 커서를 구성하는 것보다 더 쉬운 방법이 있습니다.

FrameworkElement가 있는 경우 다음 코드를 사용하여 FrameworkElement에서 커서를 작성할 수 있습니다.

public Cursor ConvertToCursor(FrameworkElement visual, Point hotSpot)
{
  int width = (int)visual.Width;
  int height = (int)visual.Height;

  // Render to a bitmap
  var bitmapSource = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
  bitmapSource.Render(visual);

  // Convert to System.Drawing.Bitmap
  var pixels = new int[width*height];
  bitmapSource.CopyPixels(pixels, width, 0);
  var bitmap = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);
  for(int y=0; y<height; y++)
    for(int x=0; x<width; x++)
      bitmap.SetPixel(x, y, Color.FromArgb(pixels[y*width+x]));

  // Save to .ico format
  var stream = new MemoryStream();
  System.Drawing.Icon.FromHandle(resultBitmap.GetHicon()).Save(stream);

  // Convert saved file into .cur format
  stream.Seek(2, SeekOrigin.Begin);
  stream.WriteByte(2);
  stream.Seek(10, SeekOrigin.Begin);
  stream.WriteByte((byte)(int)(hotSpot.X * width));
  stream.WriteByte((byte)(int)(hotSpot.Y * height));
  stream.Seek(0, SeekOrigin.Begin);

  // Construct Cursor
  return new Cursor(stream);
}

FrameworkElement의 크기는 표준 커서 크기(16x16 또는 32x32 등)여야 합니다.다음은 예를 제시하겠습니다.

<Grid x:Name="customCursor" Width="32" Height="32">
  ...
</Grid>

다음과 같이 사용됩니다.

someControl.Cursor = ConvertToCursor(customCursor, new Point(0.5, 0.5));

는 FrameworkElement가 될 수.<Image>기존 이미지가 있는지 제어하거나 WPF의 기본 제공 그리기 도구를 사용하여 원하는 모든 그림을 그릴 수 있습니다.

.cur 파일 형식에 대한 자세한 내용은 ICO(파일 형식)에서 확인할 수 있습니다.

XAML에서 커스텀 커서를 사용하기 위해 Ben McIntosh가 제공한 코드를 약간 변경했습니다.

<Window.Resources>    
 <Cursor x:Key="OpenHandCursor">Resources/openhand.cur</Cursor>
</Window.Resources>

리소스를 참조하기 위해 커서를 사용하려면:

<StackPanel Cursor="{StaticResource OpenHandCursor}" />

UIElement 자체를 커서로 찾고 있는 사람을 위해 Ray와 Arcturus솔루션을 조합했습니다.

    public Cursor ConvertToCursor(UIElement control, Point hotSpot)
    {
        // convert FrameworkElement to PNG stream
        var pngStream = new MemoryStream();
        control.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
        Rect rect = new Rect(0, 0, control.DesiredSize.Width, control.DesiredSize.Height);
        RenderTargetBitmap rtb = new RenderTargetBitmap((int)control.DesiredSize.Width, (int)control.DesiredSize.Height, 96, 96, PixelFormats.Pbgra32);

        control.Arrange(rect);
        rtb.Render(control);

        PngBitmapEncoder png = new PngBitmapEncoder();
        png.Frames.Add(BitmapFrame.Create(rtb));
        png.Save(pngStream);

        // write cursor header info
        var cursorStream = new MemoryStream();
        cursorStream.Write(new byte[2] { 0x00, 0x00 }, 0, 2);                               // ICONDIR: Reserved. Must always be 0.
        cursorStream.Write(new byte[2] { 0x02, 0x00 }, 0, 2);                               // ICONDIR: Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid
        cursorStream.Write(new byte[2] { 0x01, 0x00 }, 0, 2);                               // ICONDIR: Specifies number of images in the file.
        cursorStream.Write(new byte[1] { (byte)control.DesiredSize.Width }, 0, 1);          // ICONDIRENTRY: Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
        cursorStream.Write(new byte[1] { (byte)control.DesiredSize.Height }, 0, 1);         // ICONDIRENTRY: Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.
        cursorStream.Write(new byte[1] { 0x00 }, 0, 1);                                     // ICONDIRENTRY: Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
        cursorStream.Write(new byte[1] { 0x00 }, 0, 1);                                     // ICONDIRENTRY: Reserved. Should be 0.
        cursorStream.Write(new byte[2] { (byte)hotSpot.X, 0x00 }, 0, 2);                    // ICONDIRENTRY: Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
        cursorStream.Write(new byte[2] { (byte)hotSpot.Y, 0x00 }, 0, 2);                    // ICONDIRENTRY: Specifies the vertical coordinates of the hotspot in number of pixels from the top.
        cursorStream.Write(new byte[4] {                                                    // ICONDIRENTRY: Specifies the size of the image's data in bytes
                                          (byte)((pngStream.Length & 0x000000FF)),
                                          (byte)((pngStream.Length & 0x0000FF00) >> 0x08),
                                          (byte)((pngStream.Length & 0x00FF0000) >> 0x10),
                                          (byte)((pngStream.Length & 0xFF000000) >> 0x18)
                                       }, 0, 4);
        cursorStream.Write(new byte[4] {                                                    // ICONDIRENTRY: Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file
                                          (byte)0x16,
                                          (byte)0x00,
                                          (byte)0x00,
                                          (byte)0x00,
                                       }, 0, 4);

        // copy PNG stream to cursor stream
        pngStream.Seek(0, SeekOrigin.Begin);
        pngStream.CopyTo(cursorStream);

        // return cursor stream
        cursorStream.Seek(0, SeekOrigin.Begin);
        return new Cursor(cursorStream);
    }

매우 쉬운 방법은 Visual Studio에 커서를 .cur 파일로 생성한 다음 프로젝트 리소스에 추가하는 것입니다.

그런 다음 커서를 할당할 때 다음 코드를 추가합니다.

myCanvas.Cursor = new Cursor(new System.IO.MemoryStream(myNamespace.Properties.Resources.Cursor1));

프로젝트 리소스에서 커스텀 커서 파일을 로드하려고 했는데 비슷한 문제가 발생했습니다.인터넷을 검색하여 해결책을 찾았지만 필요한 것을 찾지 못했습니다.this.Cursor실행 시 프로젝트의 리소스 폴더에 저장된 커스텀 커서로 이동합니다.Ben의 Xaml 솔루션을 사용해 보았지만, 충분히 우아하지는 않았습니다.피터 알렌

Lamely, 상대 경로 또는 Pack URI는 기능하지 않습니다.상대 경로 또는 어셈블리로 채워진 리소스에서 커서를 로드해야 할 경우 파일에서 스트림을 가져와 커서(Stream cursorStream) 생성자에게 전달해야 합니다.짜증나지만 사실이야.

이렇게 하는 좋은 방법이 생각나서 문제를 해결했습니다.

    System.Windows.Resources.StreamResourceInfo info = 
        Application.GetResourceStream(new 
        Uri("/MainApp;component/Resources/HandDown.cur", UriKind.Relative));

    this.Cursor = new System.Windows.Input.Cursor(info.Stream); 

MainApp응용 프로그램 이름으로 대체해야 합니다. Resources프로젝트 내의 *.cur 파일에 대한 상대 폴더 경로로 대체해야 합니다.

Ray와 다소 비슷하지만 느리고 번거로운 픽셀 복사 대신 다음과 같은 Windows 내부 솔루션을 사용합니다.

private struct IconInfo {
  public bool fIcon;
  public int xHotspot;
  public int yHotspot;
  public IntPtr hbmMask;
  public IntPtr hbmColor;
}

[DllImport("user32.dll")]
private static extern IntPtr CreateIconIndirect(ref IconInfo icon);

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetIconInfo(IntPtr hIcon, ref IconInfo pIconInfo);

public Cursor ConvertToCursor(FrameworkElement cursor, Point HotSpot) {
  cursor.Arrange(new Rect(new Size(cursor.Width, cursor.Height)));
  var bitmap = new RenderTargetBitmap((int)cursor.Width, (int)cursor.Height, 96, 96, PixelFormats.Pbgra32);
  bitmap.Render(cursor);

  var info = new IconInfo();
  GetIconInfo(bitmap.ToBitmap().GetHicon(), ref info);
  info.fIcon = false;
  info.xHotspot = (byte)(HotSpot.X * cursor.Width);
  info.yHotspot = (byte)(HotSpot.Y * cursor.Height);

  return CursorInteropHelper.Create(new SafeFileHandle(CreateIconIndirect(ref info), true));
}

이러한 경우 확장 클래스에서 사용하는 확장 메서드가 중간에 있습니다.

using DW = System.Drawing;

public static DW.Bitmap ToBitmap(this BitmapSource bitmapSource) {
  var bitmap = new DW.Bitmap(bitmapSource.PixelWidth, bitmapSource.PixelHeight, DW.Imaging.PixelFormat.Format32bppPArgb);
  var data = bitmap.LockBits(new DW.Rectangle(DW.Point.Empty, bitmap.Size), DW.Imaging.ImageLockMode.WriteOnly, DW.Imaging.PixelFormat.Format32bppPArgb);
  bitmapSource.CopyPixels(Int32Rect.Empty, data.Scan0, data.Height * data.Stride, data.Stride);
  bitmap.UnlockBits(data);
  return bitmap;
}

이 모든 것을 고려하면, 그것은 다소 간단하고 간단하다.

또, 독자적인 핫 스팟을 지정할 필요가 없는 경우는, 이것을 짧게 할 수도 있습니다(구조나 P/Incokes도 필요 없습니다).

public Cursor ConvertToCursor(FrameworkElement cursor, Point HotSpot) {
  cursor.Arrange(new Rect(new Size(cursor.Width, cursor.Height)));
  var bitmap = new RenderTargetBitmap((int)cursor.Width, (int)cursor.Height, 96, 96, PixelFormats.Pbgra32);
  bitmap.Render(cursor);
  var icon = System.Drawing.Icon.FromHandle(bitmap.ToBitmap().GetHicon());
  return CursorInteropHelper.Create(new SafeFileHandle(icon.Handle, true));
}

이거 드셔보세요

<Window Cursor=""C:\WINDOWS\Cursors\dinosaur.ani"" />

다음과 같은 코드로 이를 수행할 수 있습니다.

this.Cursor = new Cursor(@"<your address of icon>");

Scott Hanselman의 BabySmash(www.codeplex.com/babysmash)도 확인하세요.그는 윈도우 커서를 숨기고 캔버스에 그의 새로운 커서를 보여준 다음 커서를 움직이면 "실제" 커서가 될 수 있는 더 "brute force" 방법을 사용했다.

자세한 내용은 이쪽:http://www.hanselman.com/blog/DeveloperDesigner.aspx

Visual Studio를 사용하시는 분들은

  1. 커서 파일 새로 만들기
  2. 이미지 복사/붙여넣기
  3. .cur 파일에 저장합니다.

Visual Studio 2017에서 변경되었지만 .cur 파일을 내장 리소스로 참조할 수 있었습니다.

<Setter
    Property="Cursor"
    Value="/assembly-name;component/location-name/curser-name.cur" />

GDI 자원(BMP 등)이 있는지 확인합니다.GetHIcon)이 폐기됩니다.그렇지 않으면 메모리 누수가 발생합니다.다음의 코드(아이콘의 확장 방식)는, WPF 에 완전하게 기능합니다.오른쪽 하단에 작은 아이콘과 함께 화살표 커서가 생성됩니다.

비고: 이 코드는 아이콘을 사용하여 커서를 만듭니다.현재 UI 컨트롤을 사용하지 않습니다.

    public static Cursor CreateCursor(this Icon icon, bool includeCrossHair, System.Drawing.Color crossHairColor)
    {
        if (icon == null)
            return Cursors.Arrow;

        // create an empty image
        int width = icon.Width;
        int height = icon.Height;

        using (var cursor = new Bitmap(width * 2, height * 2))
        {
            // create a graphics context, so that we can draw our own cursor
            using (var gr = System.Drawing.Graphics.FromImage(cursor))
            {
                // a cursor is usually 32x32 pixel so we need our icon in the lower right part of it
                gr.DrawIcon(icon, new Rectangle(width, height, width, height));

                if (includeCrossHair)
                {
                    using (var pen = new System.Drawing.Pen(crossHairColor))
                    {
                        // draw the cross-hair
                        gr.DrawLine(pen, width - 3, height, width + 3, height);
                        gr.DrawLine(pen, width, height - 3, width, height + 3);
                    }
                }
            }

            try
            {
                using (var stream = new MemoryStream())
                {
                    // Save to .ico format
                    var ptr = cursor.GetHicon();
                    var tempIcon = Icon.FromHandle(ptr);
                    tempIcon.Save(stream);

                    int x = cursor.Width/2;
                    int y = cursor.Height/2;

                    #region Convert saved stream into .cur format

                    // set as .cur file format
                    stream.Seek(2, SeekOrigin.Begin);
                    stream.WriteByte(2);

                    // write the hotspot information
                    stream.Seek(10, SeekOrigin.Begin);
                    stream.WriteByte((byte)(width));
                    stream.Seek(12, SeekOrigin.Begin);
                    stream.WriteByte((byte)(height));
                    
                    // reset to initial position
                    stream.Seek(0, SeekOrigin.Begin);

                    #endregion


                    DestroyIcon(tempIcon.Handle);  // destroy GDI resource

                    return new Cursor(stream);
                }
            }
            catch (Exception)
            {
                return Cursors.Arrow;
            }
        }
    }

    /// <summary>
    /// Destroys the icon.
    /// </summary>
    /// <param name="handle">The handle.</param>
    /// <returns></returns>
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public extern static Boolean DestroyIcon(IntPtr handle);

그러면 프로젝트에 저장된 이미지가 첨부된 속성을 사용하여 커서로 변환됩니다.이미지를 리소스로 컴파일해야 합니다!

<Button MyLibrary:FrameworkElementExtensions.Cursor=""{MyLibrary:Uri MyAssembly, MyImageFolder/MyImage.png}""/>

프레임워크 요소내선번호

using System;
using System.Windows;
using System.Windows.Media;

public static class FrameworkElementExtensions
{
    #region Cursor

    public static readonly DependencyProperty CursorProperty = DependencyProperty.RegisterAttached("Cursor", typeof(Uri), typeof(FrameworkElementExtensions), new UIPropertyMetadata(default(Uri), OnCursorChanged));
    public static Uri GetCursor(FrameworkElement i) => (Uri)i.GetValue(CursorProperty);
    public static void SetCursor(FrameworkElement i, Uri input) => i.SetValue(CursorProperty, input);
    static void OnCursorChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (sender is FrameworkElement frameworkElement)
        {
            if (GetCursor(frameworkElement) != null)
                frameworkElement.Cursor = new ImageSourceConverter().ConvertFromString(((Uri)e.NewValue).OriginalString).As<ImageSource>().Bitmap().Cursor(0, 0).Convert();
        }
    }

    #endregion
}

Image Source Extensions(이미지 소스 확장)

using System.Drawing;
using System.Windows.Media;
using System.Windows.Media.Imaging;

public static class ImageSourceExtensions
{
    public static Bitmap Bitmap(this ImageSource input) => input.As<BitmapSource>().Bitmap();
}

비트맵 소스 확장

using System.IO;
using System.Windows.Media.Imaging;

public static class BitmapSourceExtensions
{
    public static System.Drawing.Bitmap Bitmap(this BitmapSource input)
    {
        if (input == null)
            return null;

        System.Drawing.Bitmap result;
        using (var outStream = new MemoryStream())
        {
            var encoder = new PngBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(input));
            encoder.Save(outStream);
            result = new System.Drawing.Bitmap(outStream);
        }
        return result;
    }
}

비트맵 확장

using System;
using System.Drawing;
using System.Runtime.InteropServices;

public static class BitmapExtensions
{

    [StructLayout(LayoutKind.Sequential)]
    public struct ICONINFO
    {
        /// <summary>
        /// Specifies whether this structure defines an icon or a cursor. A value of TRUE specifies an icon; FALSE specifies a cursor. 
        /// </summary>
        public bool fIcon;

        /// <summary>
        /// Specifies the x-coordinate of a cursor's hot spot. If this structure defines an icon, the hot spot is always in the center of the icon, and this member is ignored.
        /// </summary>
        public Int32 xHotspot;

        /// <summary>
        /// Specifies the y-coordinate of the cursor's hot spot. If this structure defines an icon, the hot spot is always in the center of the icon, and this member is ignored. 
        /// </summary>
        public Int32 yHotspot;

        /// <summary>
        /// (HBITMAP) Specifies the icon bitmask bitmap. If this structure defines a black and white icon, this bitmask is formatted so that the upper half is the icon AND bitmask and the lower half is the icon XOR bitmask. Under this condition, the height should be an even multiple of two. If this structure defines a color icon, this mask only defines the AND bitmask of the icon. 
        /// </summary>
        public IntPtr hbmMask;

        /// <summary>
        /// (HBITMAP) Handle to the icon color bitmap. This member can be optional if this structure defines a black and white icon. The AND bitmask of hbmMask is applied with the SRCAND flag to the destination; subsequently, the color bitmap is applied (using XOR) to the destination by using the SRCINVERT flag. 
        /// </summary>
        public IntPtr hbmColor;
    }

    [DllImport("user32.dll")]
    static extern IntPtr CreateIconIndirect([In] ref ICONINFO piconinfo);

    [DllImport("user32.dll")]
    static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool DestroyIcon(IntPtr hIcon);

    public static System.Windows.Forms.Cursor Cursor(this Bitmap input, int hotX, int hotY)
    {
        ICONINFO Info = new ICONINFO();
        IntPtr Handle = input.GetHicon();
        GetIconInfo(Handle, out Info);

        Info.xHotspot = hotX;
        Info.yHotspot = hotY;
        Info.fIcon = false;

        IntPtr h = CreateIconIndirect(ref Info);
        return new System.Windows.Forms.Cursor(h);
    }
}

커서 확장

using Microsoft.Win32.SafeHandles;

public static class CursorExtensions
{
    public static System.Windows.Input.Cursor Convert(this System.Windows.Forms.Cursor Cursor)
    {
        SafeFileHandle h = new SafeFileHandle(Cursor.Handle, false);
        return System.Windows.Interop.CursorInteropHelper.Create(h);
    }
}

~하듯이

public static Type As<Type>(this object input) => input is Type ? (Type)input : default;

URI

using System;
using System.Windows.Markup;

public class Uri : MarkupExtension
{
    public string Assembly { get; set; } = null;

    public string RelativePath { get; set; }

    public Uri(string relativePath) : base()
    {
        RelativePath = relativePath;
    }

    public Uri(string assembly, string relativePath) : this(relativePath)
    {
        Assembly = assembly;
    }

    static Uri Get(string assemblyName, string relativePath) => new Uri($"pack://application:,,,/{assemblyName};component/{relativePath}", UriKind.Absolute);

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (Assembly == null)
            return new System.Uri(RelativePath, UriKind.Relative);

        return Get(Assembly, RelativePath);
    }
}

언급URL : https://stackoverflow.com/questions/46805/custom-cursor-in-wpf

반응형