简体   繁体   中英

Rotate gauge needle in C#

I cannot rotate the needle properly around its center without clipping occurring. The code I have found for rotation that doesn't result in clipping rotates the needle the wrong way. I found the code samples somewhere from stackoverflow.

Pic1 : correct position pointing south

Pic2 : RotateBitmap4(), rotation at wrong position, no clipping

Pic3 : RotateBitmap5(), correct rotation point but clipping

Pic4 : RotateBitmap5(), correct rotation point but clipping

正确位置代码段1,旋转位置错误,没有剪切代码段2,正确的旋转点但剪切代码段3,正确的旋转点但剪切

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

namespace Stackoverflow
{
    /// <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 is :

<Window x:Class="Stackoverflow.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>

指南针圈指南针

Thanks for the very useful code example. It made understanding your problem much easier.

So, what's the answer?

Well, I have a couple of suggestions. First, you're really going about the whole bitmap stuff the wrong way, by using the System.Drawing namespace, which is primarily designed to support Winforms. Hence the call to CreateBitmapSourceFromHBitmap() to map the result of all your Winforms code to a WPF-compatible bitmap.

Second, the basic issue in your existing code is that if you want to create a new bitmap in which the overlay has been rotated, you have to make sure you position the rotated image within that bitmap so that it fits entirely.

In the code you posted, you're only repositioning the arrow bitmap back to its original position after rotation rather than shifting it far enough to give it enough margin to account for the new after-rotation footprint, and so of course when it's not oriented vertically, any portion that is drawn more than half its original width to the left is off the edge of the bitmap. That's what causes the clipping.

Now, you could fix the code so that that problem was addressed, while still preserving the basic idea. But IMHO that implementation is simply wasteful. If you're going to draw the arrow image rotated, you might as well do it at the same time you composite it with the original image. No need to create yet another intermediate bitmap just for the rotation.

So, if you feel you must use the Winforms/GDI+ stuff, then IMHO this version of the code would be much better:

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;
        }
    }
}

(For my own testing purposes, I removed the overall resizing logic. I trust that given the above example, you can adjust the actual size to suit your needs).


As I mentioned in my first suggestion, it would really be better if you just did this "the WPF way". Here is an example of what that would look like:

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;
    }
}

Note that in this example, the bulk of the logic is actually in the XAML. The C# code-behind is very simple, and amounts only to exposing a dependency property to which the XAML can bind, and then the button click handler that actually updates that value.

The XAML itself is actually fairly simply too. Of course, you may want to include some additional sizing/formatting markup to get the images just the right size, position, etc. But as you can see, as far as the basic layout, you are letting WPF do all the work, including figuring out how to rotate the bitmap, and scaling it to fit the underlying dial graphic.

When compositing bitmaps, I prefer to take advantage of the alpha channel, so in the second example above, I edited your original bitmap so that the black area was removed, making that area transparent. That bitmap looks like this:
透明箭头
This ensures that no matter what the background, only the yellow arrow obscures the image.

Note that for a simple graphic like the arrow, in WPF you don't even need to use a bitmap. You can define a Path object to represent the shape. The primary advantage of doing it that way is that the graphic will scale arbitrarily without any pixelation, as a bitmap would have.

For that matter, you could even use a combination of line shapes and text, with appropriate transforms to place them correctly, as the background image. And then the background image would be able to scale arbitrarily as well without any loss of quality.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM