在 C# 中旋转仪表指针

Rotate gauge needle in C#

我无法在不发生剪裁的情况下围绕其中心正确旋转针。我找到的不会导致剪裁的旋转代码以错误的方式旋转了针。我在 Whosebug 的某处找到了代码示例。

Pic1 : 正确的位置指向南方

Pic2 : RotateBitmap4(), 旋转位置错误,没有裁剪

Pic3 : RotateBitmap5(), 旋转点正确但是裁剪

Pic4 : RotateBitmap5(), 旋转点正确但是裁剪

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows;
using System.Windows.Media.Imaging;
using Point = System.Drawing.Point;

namespace Whosebug
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
public partial class MainWindow : Window
{
    private float _angle = 0;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonTestImage_OnClick(object sender, RoutedEventArgs e)
    {
        var backgroundImage = new Bitmap(@"");//Compass circle
        var foregroundImage = new Bitmap(@"");//Compass needle
        foregroundImage = ResizeBitmap(foregroundImage, 13, 65);

        //Wrong rotation, no clipping
        //foregroundImage = RotateBitmap4(foregroundImage, _angle);

        //Correct rotation, clipping!
        foregroundImage = RotateBitmap5(foregroundImage, _angle);
        var finalImage = new Bitmap(320, 240);
        using (var graphics = Graphics.FromImage(finalImage))
        {
            //set background color
            graphics.Clear(System.Drawing.Color.Black);

            graphics.DrawImage(backgroundImage, new System.Drawing.Rectangle(0, 0, backgroundImage.Width, backgroundImage.Height));
            //graphics.DrawImage(foregroundImage, new System.Drawing.Rectangle(int.Parse(TextBoxXOffset.Text), int.Parse(TextBoxYOffset.Text), foregroundImage.Width, foregroundImage.Height));
            graphics.DrawImage(foregroundImage, new System.Drawing.Rectangle(44, 18, foregroundImage.Width, foregroundImage.Height));
        }
        var image = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(finalImage.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromWidthAndHeight(320, 240));
        ImageTest.Source = image;

        _angle += 20;
        if (_angle >= 360)
        {
            _angle = 0;
        }
    }

    private static Bitmap ResizeBitmap(Bitmap sourceBitmap, int width, int height)
    {
        var result = new Bitmap(width, height);
        using (var graphics = Graphics.FromImage(result))
        {
            graphics.DrawImage(sourceBitmap, 0, 0, width, height);
        }
        return result;
    }

    private Bitmap RotateBitmap5(Bitmap b, float angle)
    {
        //Create a new empty bitmap to hold rotated image.
        //Bitmap returnBitmap = new Bitmap(b.Width, b.Height);
        Bitmap returnBitmap = new Bitmap(b.Height + 500, b.Height + 500);
        //Make a graphics object from the empty bitmap.
        Graphics g = Graphics.FromImage(returnBitmap);
        //move rotation point to center of image.
        g.InterpolationMode = InterpolationMode.HighQualityBicubic;
        g.TranslateTransform((float)b.Width / 2, (float)b.Height / 2);
        //Rotate.        
        g.RotateTransform(angle);
        //Move image back.
        g.TranslateTransform(-(float)b.Width / 2, -(float)b.Height / 2);
        //Draw passed in image onto graphics object.
        g.DrawImage(b, new Point(0, 0));
        return returnBitmap;
    }

    public Bitmap RotateBitmap4(Bitmap b, float angle)
    {
        if (angle > 0)
        {
            int l = b.Width;
            int h = b.Height;
            double an = angle * Math.PI / 180;
            double cos = Math.Abs(Math.Cos(an));
            double sin = Math.Abs(Math.Sin(an));
            int nl = (int)(l * cos + h * sin);
            int nh = (int)(l * sin + h * cos);
            Bitmap returnBitmap = new Bitmap(nl, nh);
            Graphics g = Graphics.FromImage(returnBitmap);
            g.TranslateTransform((float)(nl - l) / 2, (float)(nh - h) / 2);
            g.TranslateTransform((float)b.Width / 2, (float)b.Height / 2);
            g.RotateTransform(angle);
            g.TranslateTransform(-(float)b.Width / 2, -(float)b.Height / 2);
            g.DrawImage(b, new Point(0, 0));
            return returnBitmap;
        }
        else return b;
    }
}
}

XAML 是:

<Window x:Class="Whosebug.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="432" Width="782">
<Grid>
    <Image Name="ImageTest" HorizontalAlignment="Left" Height="260" Margin="22,170,0,0" VerticalAlignment="Top" Width="348"/>
    <Button Name="ButtonTestImage" Content="Test Image" HorizontalAlignment="Left" Margin="490,132,0,0" VerticalAlignment="Top" Width="75" Click="ButtonTestImage_OnClick"/>
</Grid>
</Window>

感谢提供非常有用的代码示例。它使您更容易理解您的问题。

那么,答案是什么?

嗯,我有几个建议。首先,通过使用主要设计用于支持 Winforms 的 System.Drawing 命名空间,您确实以错误的方式处理了整个位图。因此调用 CreateBitmapSourceFromHBitmap() 将所有 Winforms 代码的结果映射到 WPF 兼容位图。

其次,您现有代码中的基本问题是,如果您想创建一个新的位图,其中的叠加层已旋转,您必须确保将旋转后的图像放置在该位图中,使其完全适合.

在您发布的代码中,您只是在旋转后将箭头位图重新定位回其原始位置,而不是将其移动得足够远以使其有足够的余量来考虑新的旋转后足迹,等等当然,当它不是垂直方向时,向左绘制的超过其原始宽度一半的任何部分都会超出位图的边缘。这就是导致剪裁的原因。

现在,您可以修复代码以解决该问题,同时仍保留基本思想。但恕我直言,这种实施简直是浪费。如果要绘制旋转的箭头图像,不妨在与原始图像合成的同时进行。无需为旋转创建另一个中间位图。

所以,如果你觉得你必须使用 Winforms/GDI+ 东西,那么恕我直言,这个版本的代码会好得多:

public partial class MainWindow : Window
{
    private float _angle = 0;

    public MainWindow()
    {
        InitializeComponent();
    }

    private void ButtonTestImage_OnClick(object sender, RoutedEventArgs e)
    {
        var backgroundImage = new Bitmap(@"Assets\dial.png");//Compass circle
        System.Drawing.Size finalSize = backgroundImage.Size;
        var foregroundImage = new Bitmap(@"Assets\arrow.png");//Compass needle

        foregroundImage = new Bitmap(foregroundImage, 13, 65);

        var finalImage = new Bitmap(finalSize.Width, finalSize.Height);
        using (var graphics = Graphics.FromImage(finalImage))
        {
            graphics.DrawImage(backgroundImage, 0, 0, backgroundImage.Width, backgroundImage.Height);

            graphics.TranslateTransform(backgroundImage.Width / 2f, backgroundImage.Height / 2f);
            graphics.RotateTransform(_angle);
            graphics.TranslateTransform(foregroundImage.Width / -2f, foregroundImage.Height / -2f);

            graphics.DrawImage(foregroundImage, Point.Empty);
        }
        var image = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(finalImage.GetHbitmap(), IntPtr.Zero, System.Windows.Int32Rect.Empty,
            BitmapSizeOptions.FromWidthAndHeight(finalSize.Width, finalSize.Height));
        ImageTest.Source = image;

        _angle += 20;
        if (_angle >= 360)
        {
            _angle = 0;
        }
    }
}

(出于我自己的测试目的,我删除了整体调整大小的逻辑。我相信根据上面的示例,您可以根据需要调整实际大小)。


正如我在我的第一个建议中提到的,如果你只是这样做会更好"the WPF way"。这是一个看起来像的例子:

XAML:

<Window x:Class="TestSO30142795RotateBitmapWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <BitmapImage x:Key="backgroundImage" UriSource="Assets\dial.png"/>
    <BitmapImage x:Key="foregroundImage" UriSource="Assets\arrow.png"/>
  </Window.Resources>
  <Grid>
    <Grid Margin="22,170,0,0" HorizontalAlignment="Left" VerticalAlignment="Top">
      <Image Name="ImageTestBackground" Source="{StaticResource backgroundImage}"
           HorizontalAlignment="Left" VerticalAlignment="Top" Stretch="None"/>
      <Image Name="ImageTestForeground" Source="{StaticResource foregroundImage}"
           HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="None"
           RenderTransformOrigin="0.5,0.5">
        <Image.RenderTransform>
          <TransformGroup>
            <ScaleTransform ScaleX="1" ScaleY=".8"/>
            <RotateTransform Angle="{Binding Angle}"/>
          </TransformGroup>
        </Image.RenderTransform>
      </Image>
    </Grid>
    <Button Name="ButtonTestImage" Content="Test Image"
            HorizontalAlignment="Left" VerticalAlignment="Top"
            Margin="490,132,0,0" Width="75"
            Click="ButtonTestImage_OnClick"/>
  </Grid>
</Window>

C#:

public partial class MainWindow : Window
{
    public static readonly DependencyProperty AngleProperty =
        DependencyProperty.Register("Angle", typeof(double), typeof(MainWindow));

    public double Angle
    {
        get { return (double)GetValue(AngleProperty); }
        set { SetValue(AngleProperty, value); }
    }

    public MainWindow()
    {
        InitializeComponent();

        DataContext = this;
    }

    private void ButtonTestImage_OnClick(object sender, RoutedEventArgs e)
    {
        double angle = Angle;

        angle += 20;
        if (angle >= 360)
        {
            angle = 0;
        }

        Angle = angle;
    }
}

请注意,在此示例中,大部分逻辑实际上都在 XAML 中。 C# 代码隐藏非常简单,仅公开 XAML 可以绑定的依赖项 属性,然后是实际更新该值的按钮单击处理程序。

XAML本身其实也很简单。当然,您可能希望包含一些额外的 sizing/formatting 标记以使图像的大小、位置等恰到好处。但是如您所见,就基本布局而言,您让 WPF 完成所有工作,包括弄清楚如何旋转位图,并缩放它以适合底层表盘图形。

在合成位图时,我更喜欢利用 alpha 通道,因此在上面的第二个示例中,我编辑了您的原始位图,以便删除黑色区域,使该区域透明。该位图如下所示:

这样可以确保无论背景如何,只有黄色箭头遮挡图像。

请注意,对于像箭头这样的简单图形,在 WPF 中甚至不需要使用位图。您可以定义一个 Path 对象来表示形状。这样做的主要优点是图形可以任意缩放而没有任何像素化,就像位图那样。

就此而言,您甚至可以使用线条形状和文本的组合,并通过适当的变换将它们正确放置,作为背景图像。然后背景图像也可以任意缩放而不会损失任何质量。