I am trying to put a movable needle (pointer) on a fixed graphic of a gauge (meter). The needle is moved by using a matrix rotate on a buffered graphics. I can get the fixed graphic and the needle to show. But when I render to the screen the last placed image deletes the prior graphic. I am using a timer to get the needle animation and a track bar input to produce the movement. The needle does the exact movement I am looking for.
I just cannot get the fixed background and needle to appear at the same time.
Any ideas?
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Dial01
{
public partial class dial01Form : Form
{
// Establishes timer for graphics animation
private Timer timer01 = new Timer();
/* Establishes a graphic buffer to write to
* prior display on screen */
private Graphics myGraphics;
private BufferedGraphics myBufferedGraphics1;
// Establishes manager for embedded resources (Images)
private System.Resources.ResourceManager myRM = new
System.Resources.ResourceManager("Resources.resx",
System.Reflection.Assembly.GetExecutingAssembly());
int y = 0; // Rotation value
Graphics g,g1; // Graphics objects
public dial01Form()
{
// Establishes size of Dial01Form
this.Width = 500;
this.Height = 500;
// Gets reference to the current BufferedGraphicsContext
BufferedGraphicsContext myContext1 = BufferedGraphicsManager.Current;
// Specifically sets maximum buffer size
myContext1.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
// Sets the buffer size
myBufferedGraphics1 = myContext1.Allocate(this.CreateGraphics(),
new Rectangle(0, 0, this.Width, this.Height));
// Actvates timer and sets interval
timer01.Enabled = true;
timer01.Tick += onTimer;
timer01.Interval = 20;
timer01.Start();
// Initializes form components
InitializeComponent();
}
private void onTimer(object sender, System.EventArgs e)
{
myGraphics = this.CreateGraphics();
// Initializes graphics buffer variable
g1 = myBufferedGraphics1.Graphics;
// Clears graphic buffer with a color
g1.Clear(SystemColors.Control);
// Initializes an image variable for Dial Outline
Image dial01Outline = Dial01.Properties.Resources.DialOutline250x250;
// Draw Dial Outline to graphics buffer
myGraphics.DrawImage(dial01Outline, (ClientSize.Width / 2) - 100,
(ClientSize.Height / 2) - 100);
// Goto drawPointer method passing trackBar1 value
drawPointer(trackBar1.Value);
// Render buffered graphics to screen
// myBufferedGraphics.Render(Graphics.FromHwnd(this.Handle));
myBufferedGraphics1.Render();
}
public int drawPointer(int trkBarValue)
{
int x = trkBarValue;
y = 0;
if (225 + x <= 360) { y = 222 + x; }
else if (225 + x > 360) { y = x - 135; }
// These two labels are for testing purposes
label1.Text = ("Trk Bar Val = " + x).ToString();
label2.Text = ("Ptr value = " + y).ToString();
y = y + 180;
// Matrix rotation to pointer
Matrix myMatrix = new Matrix();
myMatrix.Rotate(y, MatrixOrder.Append);
myMatrix.Translate(this.ClientSize.Width / 2,
this.ClientSize.Height / 2, MatrixOrder.Append);
g1.Transform = myMatrix;
// Pointer polygon
PointF point1 = new PointF(0.0F, 0.0F);
PointF point2 = new PointF(0.0F, 50.0F);
PointF point3 = new PointF(3.0F, 55.0F);
PointF point4 = new PointF(7.0F, 50.0F);
PointF point5 = new PointF(7.0F, 0.0F);
PointF[] polyPoints =
{
point1,
point2,
point3,
point4,
point5
};
g1.FillPolygon(Brushes.Black, polyPoints);
return y;
}
private void dial01Form_Load(object sender, EventArgs e)
{
}
private void trackBar1_Scroll(object sender, EventArgs e)
{
}
}
}
The general graphics approach you've taken is not appropriate for a winforms app.
The way graphics works in winforms, whenever the form is covered/uncovered/resized/etc, Windows tells it to repaint itself. Anything you've done with CreateGraphics will be overwritten at this point. This is why you shouldn't call CreateGraphics.
Instead, you should intercept the repainting process via the Paint event, and do all your custom painting there. You can still repaint on a timer, you just call Invalidate() inside the timer, which causes the form to repaint as soon as it can.
This is the general shape of the "right way" to do it:
public partial class dial01Form : Form
{
private Timer timer01 = new Timer();
int y = 0; // Rotation value
public dial01Form()
{
// Establishes size of Dial01Form
this.Width = 500;
this.Height = 500;
// Actvates timer and sets interval
timer01.Enabled = true;
timer01.Tick += onTimer;
timer01.Interval = 20;
timer01.Start();
// handle the paint event
this.Paint += OnPaint;
// Initializes form components
InitializeComponent();
}
private void OnPaint(object sender, PaintEventArgs e)
{
// all painting here, targeting e.Graphics
e.Graphics.Clear(SystemColors.Control);
Image dial01Outline = Dial01.Properties.Resources.DialOutline250x250;
e.Graphics.DrawImage(dial01Outline, (ClientSize.Width / 2) - 100,
(ClientSize.Height / 2) - 100);
drawPointer(e.Graphics, trackBar1.Value);
}
private void onTimer(object sender, System.EventArgs e)
{
this.Invalidate();
}
public int drawPointer(Graphics g1, int trkBarValue)
{
// elided: same code as before, but using the g1 parameter instead of a field
}
}
You shouldn't have problems with flickering, I think - double-buffering is enabled by default. Make sure your form's DoubleBuffered property is set to True though.
The problem I am having is getting the new Bitmap to sync properly with the size of the printer output using DrawImageUnscaled() inside the PrinterDocument's PrintPage event.
I got the idea to render an image as a page collection from the comments on another post where I asked how to work with the printer in a more traditional style ( NewPage, drawing items before calling Print, etc ) which does not exist within the .NET framework. On my first attempt to use an Image collection, I noticed their was some graininess when using .DrawImage() even after setting the Bitmap Dpi to the same Dpi as the printer object, which I found was discoverable without printing using the PrinterSettings.CreateMeasurementGraphics() Graphics object through trial and error (lots of error.
The result of this endeavor thus far, is the following class (it has some 'test' code where I have been playing around, but I have cleaned up most of it so it is more presentable here)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Windows.Forms.Design;
using PdfFileWriter;
using System.Drawing.Printing;
using System.ComponentModel;
using System.IO;
class PDF : PrintDocument {
/// <summary>
/// Logo to display on invoice
/// </summary>
public Image Logo { get; set; }
/// <summary>
/// Current X position on canvas
/// </summary>
public int X { get; set; }
/// <summary>
/// Current Y position on canvas
/// </summary>
public int Y { get; set; }
/// <summary>
/// Set the folder where backups, downloads, etc will be stored or retrieved from
/// </summary>
[Editor( typeof( System.Windows.Forms.Design.FolderNameEditor ), typeof( System.Drawing.Design.UITypeEditor ) )]
public string Folder { get { return directory; } set { directory=value; } }
/// <summary>
/// Current font used to print
/// </summary>
public Font Font { get; set; }
/// <summary>
/// Current font color
/// </summary>
public Color ForeColor { get; set; }
private int CurrentPagePrinting { get; set; }
/// <summary>
/// Set printer margins
/// </summary>
public Margins PrintMargins {
get { return DefaultPageSettings.Margins; }
set { DefaultPageSettings.Margins = value; }
}
/// <summary>
/// Pages drawn in document
/// </summary>
private List<Image> Pages;
/// <summary>
/// The current selected page number. 0 if nothing selected
/// </summary>
private int CurrentPage;
/// <summary>
/// The current working directory to save files to
/// </summary>
private string directory;
/// <summary>
/// The currently chosen filename
/// </summary>
private string file;
/// <summary>
/// Public acceisble object to all paperSizes as set
/// </summary>
public List<PrintPaperSize> paperSizes { get; private set; }
/// <summary>
/// Object for holding papersizes
/// </summary>
public class PrintPaperSize {
public string Name { get; set; }
public double Height { get; set; }
public double Width { get; set; }
public PrintPaperSize() {
Height = 0;
Width = 0;
Name = "";
}
}
/// <summary>
/// Current papersize selected. used for some calculations
/// </summary>
public PrintPaperSize CurrentPaperSize { get; private set; }
public PDF() {
// set the file name without extension to something safe
file = (string)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds.ToString();
// set the save directory to MyDocuments
directory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
CurrentPage = 0;
// initialize pages array
Pages = new List<Image>();
// Set the initial font and color
Font = new System.Drawing.Font("Arial", (float)11.25);
ForeColor = Color.Black;
// set the printer to Microsoft's PDF printer and generate and ensure it will save to a file
PrinterSettings = new PrinterSettings() {
PrinterName = "Microsoft Print to PDF",
PrintToFile = true,
PrintFileName = Path.Combine(directory, file + ".pdf"),
};
// hide the notice 'printing' while spooling job.
PrintController = new StandardPrintController();
// set the printer quality to maximum so we can use this for getting the dpi at this setting
DefaultPageSettings.PrinterResolution.Kind = PrinterResolutionKind.High;
// store all paper sizes at 1 dpi [ reference: https://social.msdn.microsoft.com/Forums/vstudio/en-US/05169a47-04d5-4890-9b0a-7ad11a6a87f2/need-pixel-width-for-paper-sizes-a4-a5-executive-letter-legal-executive?forum=csharpgeneral ]
paperSizes = new List<PrintPaperSize>();
foreach ( PaperSize P in PrinterSettings.PaperSizes ) {
double W=P.Width/100.0;
double H=P.Height/100.0;
paperSizes.Add(
new PrintPaperSize() {
Height = H,
Width = W,
Name = P.PaperName
}
);
if ( P.PaperName=="Letter" ) {
CurrentPaperSize = paperSizes[paperSizes.Count-1];
}
}
// setup the initial page type, orientation, margins,
using ( Graphics g=PrinterSettings.CreateMeasurementGraphics() ) {
DefaultPageSettings = new PageSettings(PrinterSettings) {
PaperSize=new PaperSize( CurrentPaperSize.Name, (Int32)(CurrentPaperSize.Width*g.DpiX), (Int32)(CurrentPaperSize.Height*g.DpiY) ),
Landscape = false,
Margins = new Margins(left: 10, right: 10, top: 10, bottom: 10),
PrinterResolution=new PrinterResolution() {
Kind = PrinterResolutionKind.High
}
};
}
// constrain print within margins
OriginAtMargins = true;
}
public void SetPaperSize( PaperKind paperSize ) {
// TODO: Use Linq on
}
/// <summary>
/// Get specific page
/// </summary>
/// <param name="page">page number. 1 based array</param>
/// <returns></returns>
public Image GetPage( int page ) {
int p = page - 1;
if ( p<0||p>Pages.Count ) { return null; }
return Pages[p];
}
/// <summary>
/// Get the current page
/// </summary>
/// <returns>Image</returns>
public Image GetCurrentPage() {
return GetPage(CurrentPage);
}
/// <summary>
/// Before printing starts
/// </summary>
/// <param name="e">PrintEventArgs</param>
protected override void OnBeginPrint( PrintEventArgs e ) {
CurrentPagePrinting=0;
base.OnBeginPrint( e );
}
/// <summary>
/// Print page event
/// </summary>
/// <param name="e">PrintPageEventArgs</param>
protected override void OnPrintPage( PrintPageEventArgs e ) {
CurrentPagePrinting++;
// if page count is max exit print routine
if ( CurrentPagePrinting>=Pages.Count ) { e.HasMorePages=false; base.OnPrintPage( e ); return; }
// ensure high resolution / clarity of image so text doesn't fuzz
e.Graphics.CompositingMode=CompositingMode.SourceOver;
e.Graphics.CompositingQuality=CompositingQuality.HighQuality;
// Draw image and respect margins (unscaled in addition to the above so text doesn't fuzz)
e.Graphics.DrawImageUnscaled(
Pages[CurrentPagePrinting-1],
new Point(
DefaultPageSettings.Margins.Top,
DefaultPageSettings.Margins.Left
)
);
base.OnPrintPage( e );
}
/// <summary>
/// After printing has been completed
/// </summary>
/// <param name="e">PrintEventArgs</param>
protected override void OnEndPrint( PrintEventArgs e ) {
base.OnEndPrint( e );
}
/// <summary>
/// Add a new page to the document
/// </summary>
public void NewPage() {
// Add a new page to the page collection and set it as the current page
Bitmap bmp;
using(Graphics g = PrinterSettings.CreateMeasurementGraphics()) {
float dpiscaleX;
float dpiscaleY;
// measure default bitmap dpi on this system and use to calculate print dpi
using ( Bitmap b=new Bitmap( 1, 1 ) ) {
dpiscaleX = b.HorizontalResolution;
dpiscaleY = b.VerticalResolution;
};
bmp = new Bitmap(
(Int32)(((DefaultPageSettings.PrintableArea.Width-( DefaultPageSettings.Margins.Left+DefaultPageSettings.Margins.Right )) / dpiscaleX) * g.DpiX),
(Int32)(((DefaultPageSettings.PrintableArea.Height-( DefaultPageSettings.Margins.Top+DefaultPageSettings.Margins.Bottom )) / dpiscaleY) * g.DpiY)
);
bmp.SetResolution(g.DpiX, g.DpiY);
}
Pages.Add( bmp );
CurrentPage++;
}
/// <summary>
/// Add a new string to the current page
/// </summary>
/// <param name="text">The string to print</param>
/// <param name="align">Optional alignment of the string</param>
public void DrawString(string text, System.Windows.TextAlignment align = System.Windows.TextAlignment.Left ) {
// add string to document
using ( Graphics g=Graphics.FromImage( Pages[CurrentPage - 1] ) ) {
g.CompositingQuality = CompositingQuality.HighQuality;
switch ( align ) {
case System.Windows.TextAlignment.Left:
case System.Windows.TextAlignment.Justify:
g.DrawString( text, Font, new SolidBrush( ForeColor ), new PointF( X, Y ) );
Y+=(Int32)g.MeasureString( "X", Font ).Height;
break;
case System.Windows.TextAlignment.Right:
g.DrawString( text, Font, new SolidBrush( ForeColor ), new PointF( Pages[CurrentPage - 1].Width - g.MeasureString( text, Font ).Width, Y ) );
Y += (Int32)g.MeasureString( "X", Font ).Height;
break;
case System.Windows.TextAlignment.Center:
g.DrawString( text, Font, new SolidBrush( ForeColor ), new PointF( ( Pages[CurrentPage-1].Width+g.MeasureString( text, Font ).Width )/2, Y ) );
Y+=(Int32)g.MeasureString( "X", Font ).Height;
break;
}
}
}
}
Illustration of PDF output
As you can see, the canvas (Page property in my class), goes out of bounds. If I draw the image using just .DrawImage() the scaling stretches it and it just looks grainy, so I must use .DrawImageUnscaled()
The lines above in order are the result of the following code :
// Initialize the custom print class
PDF p = new PDF();
// Add a new page to the document
p.NewPage();
// Draw some strings. p.Y value is automatically incremented
p.DrawString( "Hello" );
p.DrawString( "Hello", System.Windows.TextAlignment.Right );
p.DrawString( "Hello", System.Windows.TextAlignment.Center );
p.DrawString( "Hello pure awesomeness" );
// Uncomment the following and add a picture box to the form
// pictureBox1.Height = 1100;
// pictureBox1.Width = 850;
// pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
// pictureBox1.Image = p.GetCurrentPage();
// Send all pages to the "printer"
p.Print();
If you uncomment the pictureBox1 lines, and comment out p.Print(), the result is correct (keeping in mind the image is smaller than the printdocument page due to the margins being set for the page.
Illustration of PictureBox output
And if you use the following code (in place of the above pictureBox code) which is functionally equivalent to the Dpi scaling inside the class, everything shows up properly in the pictureBox (just a lot larger as my 'High' setting resolves to 600 DPI where a newly created Bitmap is at 72 DPI before calling the SetResolution() method on the Image.
Image img = p.GetCurrentPage();
pictureBox1.Height=(Int32)(p.CurrentPaperSize.Height*img.VerticalResolution);
pictureBox1.Width = (Int32)(p.CurrentPaperSize.Width*img.HorizontalResolution);
pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
pictureBox1.Image = img;
The calculation for measuring DefaultPageSettings.Margins was wrong as Margins are represented in 100th's of an inch according to this MSDN document. The correct way to adjust for this is to divide by 100.
x = DefaultPageSettings.Margins/100
Following is the OP's NewPage() method rewritten with the proper calculations :
/// <summary>
/// Add a new page to the document
/// </summary>
public void NewPage() {
// Add a new page to the page collection and set it as the current page
Bitmap bmp;
using(Graphics g = PrinterSettings.CreateMeasurementGraphics()) {
int w=(Int32)( CurrentPaperSize.Width*g.DpiX )-(Int32)( ( ( DefaultPageSettings.Margins.Left+DefaultPageSettings.Margins.Right )/100 )*g.DpiX );
int h=(Int32)( CurrentPaperSize.Height*g.DpiY )-(Int32)( ( ( DefaultPageSettings.Margins.Top+DefaultPageSettings.Margins.Bottom )/100 )*g.DpiY );
bmp = new Bitmap( w, h );
bmp.SetResolution(g.DpiX, g.DpiY);
}
// reset X and Y positions
Y=0;
X=0;
// Add new page to the collection
Pages.Add( bmp );
CurrentPage++;
}
I'm currently getting scanned pages through Twain and transforming the pages to Bitmap, using the BitmapRenderer of twaindotnet project, as described in this post.
My scanner allows me to scan recto and verso.
When I scan recto only pages, it works like a charm: the generated bitmaps are perfect. But when it scans recto-verso, the bitmap are flipped . Sometimes vertically, sometimes horizontally.
I can't use the Bitmap.RotateFlip() method because the effect doesn't concern each picture, but only when recto-verso pages.
I've tried the Bitmap.FromHbitmap() described here or the default constructor, but it throws an error related to GDI+.
I'm pretty sure the issue is where the bitmap is converted from the pointer, in the BitmapRenderer class. Here is the code (I did not include the Dispose() methods for clarity purpose) :
public class BitmapRenderer : IDisposable
{
private readonly IntPtr _picturePointer;
private readonly IntPtr _bitmapPointer;
private readonly IntPtr _pixelInfoPointer;
private Rectangle _rectangle;
private readonly BitmapInfoHeader _bitmapInfo;
/// <summary>
/// Initializes a new instance of the <see cref="BitmapRenderer"/> class.
/// </summary>
/// <param name="picturePointer_">The picture pointer.</param>
public BitmapRenderer(IntPtr picturePointer_)
{
_picturePointer = picturePointer_;
_bitmapPointer = Kernel32Native.GlobalLock(picturePointer_);
_bitmapInfo = new BitmapInfoHeader();
Marshal.PtrToStructure(_bitmapPointer, _bitmapInfo);
_rectangle = new Rectangle();
_rectangle.X = _rectangle.Y = 0;
_rectangle.Width = _bitmapInfo.Width;
_rectangle.Height = _bitmapInfo.Height;
if (_bitmapInfo.SizeImage == 0)
{
_bitmapInfo.SizeImage = ((((_bitmapInfo.Width*_bitmapInfo.BitCount) + 31) & ~31) >> 3)*
_bitmapInfo.Height;
}
// The following code only works on x86
Debug.Assert(Marshal.SizeOf(typeof (IntPtr)) == 4);
int pixelInfoPointer = _bitmapInfo.ClrUsed;
if ((pixelInfoPointer == 0) && (_bitmapInfo.BitCount <= 8))
pixelInfoPointer = 1 << _bitmapInfo.BitCount;
pixelInfoPointer = (pixelInfoPointer*4) + _bitmapInfo.Size + _bitmapPointer.ToInt32();
_pixelInfoPointer = new IntPtr(pixelInfoPointer);
}
/// <summary>
/// Renders to bitmap.
/// </summary>
/// <returns></returns>
public Bitmap RenderToBitmap()
{
Bitmap bitmap = new Bitmap(_rectangle.Width, _rectangle.Height);
using (Graphics graphics = Graphics.FromImage(bitmap))
{
IntPtr hdc = graphics.GetHdc();
try
{
Gdi32Native.SetDIBitsToDevice(hdc, 0, 0, _rectangle.Width, _rectangle.Height,
0, 0, 0, _rectangle.Height, _pixelInfoPointer, _bitmapPointer, 0);
}
finally
{
graphics.ReleaseHdc(hdc);
}
}
bitmap.SetResolution(PpmToDpi(_bitmapInfo.XPelsPerMeter), PpmToDpi(_bitmapInfo.YPelsPerMeter));
return bitmap;
}
private static float PpmToDpi(double pixelsPerMeter_)
{
double pixelsPerMillimeter = pixelsPerMeter_/1000.0;
double dotsPerInch = pixelsPerMillimeter*25.4;
return (float) Math.Round(dotsPerInch, 2);
}
I don't understand where this is from or how to solve it.
EDIT
Well, it appears this situation is not related to twain conversion to bitmap (the issue is not from twaindotnet project at all).
It only occurs with handwritten pages. This is an automatic OCR issue.
Does someone know how to disable OCR for handwritten document ?
How would I go about finding out how a Windows Forms Control was made? I want to create a Control from scratch. Preferably a ListBox or even better, a ListView Control, but I have no idea where to start.
Some suggestions I've come across in the past have been:
Use a Panel Control and dynamically add Label controls to it with appropriate styling, and;
Extend or Subclass the ListView/ListBox Controls, and set OwnerDraw to true, and do your custom drawing in the OnPaint event.
But I want more control than that. I don't just want a ListView Control, I don't want to use a third-party control either (no matter how good [Object ListView] is1. I want my own ListView Control. I don't care how hard it is, but is this possible in Windows Forms? Where should I start?
Would I need to use GDI/GDI+ to draw everything? Would I start with an empty Panel Control and then manually draw each List Item using the System.Drawing namespace?
How would I go about finding out how a Windows Forms Control was made?
Simple, Every control is a Window created using CreateWindowEx method (done internally by the Winforms).
In winforms point of view: Control is the base class for all Windows. There are some controls which has been written in unmanaged code like ListView, ListBox etc. For them you can't see the paint code in .net. It is implemented in OS itself(not sure which dll they live). Winforms just provides a wrapper over those unmanaged controls.
But, there are purely managed controls written in c#. Example: DataGridView. You can go through the code. Here master is OnPaint protected method. That is the place where you need to write all your custom painting logic with the Graphics instance provided.
Key is you'll create a "Datastructure" which holds all the necessary items to draw your control. Lets say ItemRectangle, Text, Color, Font, etc.. Then you use them all together to paint your custom control in OnPaint method.
Would I need to use GDI/GDI+ to draw everything?
You'll use System.Drawing and System.Drawing.Drawing2D namespaces to draw your control. If something that .net doesn't provide you'll p/invoke Gdi/Gdi+
Advice for choosing base class: If your control needs to be scrollable(ListView kind of controls likely need it). So you can choose ScrollableControl or Panel as base class which supports scrolling. Otherwise you can inherit from Control class.
Developing Custom Windows Forms Controls with the .NET Framework
All the best :)
You can inherit Usercontrol and start writing from scratch or if you want specific functionalities like listview you can inherit the relevant control.
Just googling "custom listview control" or "custom control (yourtype) will give 1000's of results".
Hope this helps
eg code for Vista Style button:
public class VistaButton : System.Windows.Forms.UserControl
{
#region - Designer -
private System.ComponentModel.Container components = null;
/// <summary>
/// Initialize the component with it's
/// default settings.
/// </summary>
public VistaButton()
{
InitializeComponent();
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.DoubleBuffer, true);
this.SetStyle(ControlStyles.ResizeRedraw, true);
this.SetStyle(ControlStyles.Selectable, true);
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.SetStyle(ControlStyles.UserPaint, true);
this.BackColor = Color.Transparent;
mFadeIn.Interval = 30;
mFadeOut.Interval = 30;
}
/// <summary>
/// Release resources used by the control.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region - Component Designer generated code -
private void InitializeComponent()
{
//
// VistaButton
//
this.Name = "VistaButton";
this.Size = new System.Drawing.Size(100, 32);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.VistaButton_Paint);
this.KeyUp += new System.Windows.Forms.KeyEventHandler(this.VistaButton_KeyUp);
this.KeyDown += new System.Windows.Forms.KeyEventHandler(this.VistaButton_KeyDown);
this.MouseEnter += new System.EventHandler(this.VistaButton_MouseEnter);
this.MouseLeave += new System.EventHandler(this.VistaButton_MouseLeave);
this.MouseUp +=new MouseEventHandler(VistaButton_MouseUp);
this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.VistaButton_MouseDown);
this.GotFocus +=new EventHandler(VistaButton_MouseEnter);
this.LostFocus +=new EventHandler(VistaButton_MouseLeave);
this.mFadeIn.Tick += new EventHandler(mFadeIn_Tick);
this.mFadeOut.Tick += new EventHandler(mFadeOut_Tick);
this.Resize +=new EventHandler(VistaButton_Resize);
}
#endregion
#endregion
#region - Enums -
/// <summary>
/// A private enumeration that determines
/// the mouse state in relation to the
/// current instance of the control.
/// </summary>
enum State {None, Hover, Pressed};
/// <summary>
/// A public enumeration that determines whether
/// the button background is painted when the
/// mouse is not inside the ClientArea.
/// </summary>
public enum Style
{
/// <summary>
/// Draw the button as normal
/// </summary>
Default,
/// <summary>
/// Only draw the background on mouse over.
/// </summary>
Flat
};
#endregion
#region - Properties -
#region - Private Variables -
private bool calledbykey = false;
private State mButtonState = State.None;
private Timer mFadeIn = new Timer();
private Timer mFadeOut = new Timer();
private int mGlowAlpha = 0;
#endregion
#region - Text -
private string mText;
/// <summary>
/// The text that is displayed on the button.
/// </summary>
[Category("Text"),
Description("The text that is displayed on the button.")]
public string ButtonText
{
get { return mText; }
set { mText = value; this.Invalidate(); }
}
private Color mForeColor = Color.White;
/// <summary>
/// The color with which the text is drawn.
/// </summary>
[Category("Text"),
Browsable(true),
DefaultValue(typeof(Color),"White"),
Description("The color with which the text is drawn.")]
public override Color ForeColor
{
get { return mForeColor; }
set { mForeColor = value; this.Invalidate(); }
}
private ContentAlignment mTextAlign = ContentAlignment.MiddleCenter;
/// <summary>
/// The alignment of the button text
/// that is displayed on the control.
/// </summary>
[Category("Text"),
DefaultValue(typeof(ContentAlignment),"MiddleCenter"),
Description("The alignment of the button text " +
"that is displayed on the control.")]
public ContentAlignment TextAlign
{
get { return mTextAlign; }
set { mTextAlign = value; this.Invalidate(); }
}
#endregion
#region - Image -
private Image mImage;
/// <summary>
/// The image displayed on the button that
/// is used to help the user identify
/// it's function if the text is ambiguous.
/// </summary>
[Category("Image"),
DefaultValue(null),
Description("The image displayed on the button that " +
"is used to help the user identify" +
"it's function if the text is ambiguous.")]
public Image Image
{
get { return mImage; }
set { mImage = value; this.Invalidate(); }
}
private ContentAlignment mImageAlign = ContentAlignment.MiddleLeft;
/// <summary>
/// The alignment of the image
/// in relation to the button.
/// </summary>
[Category("Image"),
DefaultValue(typeof(ContentAlignment),"MiddleLeft"),
Description("The alignment of the image " +
"in relation to the button.")]
public ContentAlignment ImageAlign
{
get { return mImageAlign; }
set { mImageAlign = value; this.Invalidate(); }
}
private Size mImageSize = new Size(24,24);
/// <summary>
/// The size of the image to be displayed on the
/// button. This property defaults to 24x24.
/// </summary>
[Category("Image"),
DefaultValue(typeof(Size),"24, 24"),
Description("The size of the image to be displayed on the" +
"button. This property defaults to 24x24.")]
public Size ImageSize
{
get { return mImageSize; }
set { mImageSize = value; this.Invalidate(); }
}
#endregion
#region - Appearance -
private Style mButtonStyle = Style.Default;
/// <summary>
/// Sets whether the button background is drawn
/// while the mouse is outside of the client area.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Style),"Default"),
Description("Sets whether the button background is drawn " +
"while the mouse is outside of the client area.")]
public Style ButtonStyle
{
get { return mButtonStyle; }
set { mButtonStyle = value; this.Invalidate(); }
}
private int mCornerRadius = 8;
/// <summary>
/// The radius for the button corners. The
/// greater this value is, the more 'smooth'
/// the corners are. This property should
/// not be greater than half of the
/// controls height.
/// </summary>
[Category("Appearance"),
DefaultValue(8),
Description("The radius for the button corners. The " +
"greater this value is, the more 'smooth' " +
"the corners are. This property should " +
"not be greater than half of the " +
"controls height.")]
public int CornerRadius
{
get { return mCornerRadius; }
set { mCornerRadius = value; this.Invalidate(); }
}
private Color mHighlightColor = Color.White;
/// <summary>
/// The colour of the highlight on the top of the button.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "White"),
Description("The colour of the highlight on the top of the button.")]
public Color HighlightColor
{
get { return mHighlightColor; }
set { mHighlightColor = value; this.Invalidate(); }
}
private Color mButtonColor = Color.Black;
/// <summary>
/// The bottom color of the button that
/// will be drawn over the base color.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "Black"),
Description("The bottom color of the button that " +
"will be drawn over the base color.")]
public Color ButtonColor
{
get { return mButtonColor; }
set { mButtonColor = value; this.Invalidate(); }
}
private Color mGlowColor = Color.FromArgb(141,189,255);
/// <summary>
/// The colour that the button glows when
/// the mouse is inside the client area.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "141,189,255"),
Description("The colour that the button glows when " +
"the mouse is inside the client area.")]
public Color GlowColor
{
get { return mGlowColor; }
set { mGlowColor = value; this.Invalidate(); }
}
private Image mBackImage;
/// <summary>
/// The background image for the button,
/// this image is drawn over the base
/// color of the button.
/// </summary>
[Category("Appearance"),
DefaultValue(null),
Description("The background image for the button, " +
"this image is drawn over the base " +
"color of the button.")]
public Image BackImage
{
get { return mBackImage; }
set { mBackImage = value; this.Invalidate(); }
}
private Color mBaseColor = Color.Black;
/// <summary>
/// The backing color that the rest of
/// the button is drawn. For a glassier
/// effect set this property to Transparent.
/// </summary>
[Category("Appearance"),
DefaultValue(typeof(Color), "Black"),
Description("The backing color that the rest of" +
"the button is drawn. For a glassier " +
"effect set this property to Transparent.")]
public Color BaseColor
{
get { return mBaseColor; }
set { mBaseColor = value; this.Invalidate(); }
}
#endregion
#region - Behaviour -
private DialogResult mDialogResult = DialogResult.OK;
/// <summary>
/// Specify the dialog result property.
/// </summary>
[Category("Behaviour"),
DefaultValue(typeof(DialogResult)),
Description("The Dialog-Box result produced in a modal form" +
"by clicking the button.")]
public virtual DialogResult DialogResult
{
get { return mDialogResult; }
set { mDialogResult = value; this.Invalidate(); }
}
#endregion
#endregion
#region - Functions -
private GraphicsPath RoundRect(RectangleF r, float r1, float r2, float r3, float r4)
{
float x = r.X, y = r.Y, w = r.Width, h = r.Height;
GraphicsPath rr = new GraphicsPath();
rr.AddBezier(x, y + r1, x, y, x + r1, y, x + r1, y);
rr.AddLine(x + r1, y, x + w - r2, y);
rr.AddBezier(x + w - r2, y, x + w, y, x + w, y + r2, x + w, y + r2);
rr.AddLine(x + w, y + r2, x + w, y + h - r3);
rr.AddBezier(x + w, y + h - r3, x + w, y + h, x + w - r3, y + h, x + w - r3, y + h);
rr.AddLine(x + w - r3, y + h, x + r4, y + h);
rr.AddBezier(x + r4, y + h, x, y + h, x, y + h - r4, x, y + h - r4);
rr.AddLine(x, y + h - r4, x, y + r1);
return rr;
}
private StringFormat StringFormatAlignment(ContentAlignment textalign)
{
StringFormat sf = new StringFormat();
switch (textalign)
{
case ContentAlignment.TopLeft:
case ContentAlignment.TopCenter:
case ContentAlignment.TopRight:
sf.LineAlignment = StringAlignment.Near;
break;
case ContentAlignment.MiddleLeft:
case ContentAlignment.MiddleCenter:
case ContentAlignment.MiddleRight:
sf.LineAlignment = StringAlignment.Center;
break;
case ContentAlignment.BottomLeft:
case ContentAlignment.BottomCenter:
case ContentAlignment.BottomRight:
sf.LineAlignment = StringAlignment.Far;
break;
}
switch (textalign)
{
case ContentAlignment.TopLeft:
case ContentAlignment.MiddleLeft:
case ContentAlignment.BottomLeft:
sf.Alignment = StringAlignment.Near;
break;
case ContentAlignment.TopCenter:
case ContentAlignment.MiddleCenter:
case ContentAlignment.BottomCenter:
sf.Alignment = StringAlignment.Center;
break;
case ContentAlignment.TopRight:
case ContentAlignment.MiddleRight:
case ContentAlignment.BottomRight:
sf.Alignment = StringAlignment.Far;
break;
}
return sf;
}
#endregion
#region - Drawing -
/// <summary>
/// Draws the outer border for the control
/// using the ButtonColor property.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawOuterStroke(Graphics g)
{
Color buttonColor = this.ButtonColor;
if (!this.Enabled)
buttonColor = System.Drawing.SystemColors.ControlDark;
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
Rectangle r = this.ClientRectangle;
r.Width -= 1; r.Height -= 1;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
using (Pen p = new Pen(buttonColor))
{
g.DrawPath(p, rr);
}
}
}
/// <summary>
/// Draws the inner border for the control
/// using the HighlightColor property.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawInnerStroke(Graphics g)
{
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
Rectangle r = this.ClientRectangle;
r.X++; r.Y++;
r.Width -= 3; r.Height -= 3;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
using (Pen p = new Pen(this.HighlightColor))
{
g.DrawPath(p, rr);
}
}
}
/// <summary>
/// Draws the background for the control
/// using the background image and the
/// BaseColor.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawBackground(Graphics g)
{
Color baseColor = this.BaseColor;
Color buttonColor = this.ButtonColor;
if (!this.Enabled)
{
baseColor = SystemColors.Control;
buttonColor = System.Drawing.SystemColors.Control;
}
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
int alpha = (mButtonState == State.Pressed) ? 204 : 127;
Rectangle r = this.ClientRectangle;
r.Width--; r.Height--;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
using (SolidBrush sb = new SolidBrush(baseColor))
{
g.FillPath(sb, rr);
}
SetClip(g);
if (this.BackImage != null){g.DrawImage(this.BackImage, this.ClientRectangle);}
g.ResetClip();
using (SolidBrush sb = new SolidBrush(Color.FromArgb(alpha, buttonColor)))
{
g.FillPath(sb, rr);
}
}
}
/// <summary>
/// Draws the Highlight over the top of the
/// control using the HightlightColor.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawHighlight(Graphics g)
{
if (this.ButtonStyle == Style.Flat && this.mButtonState == State.None){return;}
int alpha = (mButtonState == State.Pressed) ? 60 : 150;
Rectangle rect = new Rectangle(0, 0, this.Width, this.Height / 2);
using (GraphicsPath r = RoundRect(rect, CornerRadius, CornerRadius, 0, 0))
{
using (LinearGradientBrush lg = new LinearGradientBrush(r.GetBounds(),
Color.FromArgb(alpha, this.HighlightColor),
Color.FromArgb(alpha / 3, this.HighlightColor),
LinearGradientMode.Vertical))
{
g.FillPath(lg, r);
}
}
}
/// <summary>
/// Draws the glow for the button when the
/// mouse is inside the client area using
/// the GlowColor property.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawGlow(Graphics g)
{
if (this.mButtonState == State.Pressed){return;}
SetClip(g);
using (GraphicsPath glow = new GraphicsPath())
{
glow.AddEllipse(-5,this.Height / 2 - 10, this.Width + 11, this.Height + 11);
using (PathGradientBrush gl = new PathGradientBrush(glow))
{
gl.CenterColor = Color.FromArgb(mGlowAlpha, this.GlowColor);
gl.SurroundColors = new Color[] {Color.FromArgb(0, this.GlowColor)};
g.FillPath(gl, glow);
}
}
g.ResetClip();
}
/// <summary>
/// Draws the text for the button.
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawText(Graphics g)
{
Color foreColor = this.ForeColor;
if (!this.Enabled)
foreColor = System.Drawing.SystemColors.ControlDark;
StringFormat sf = StringFormatAlignment(this.TextAlign);
Rectangle r = new Rectangle(8,8,this.Width - 17,this.Height - 17);
g.DrawString(this.ButtonText, this.Font, new SolidBrush(foreColor), r, sf);
}
/// <summary>
/// Draws the image for the button
/// </summary>
/// <param name="g">The graphics object used in the paint event.</param>
private void DrawImage(Graphics g)
{
if (this.Image == null) {return;}
Rectangle r = new Rectangle(8,8,this.ImageSize.Width,this.ImageSize.Height);
switch (this.ImageAlign)
{
case ContentAlignment.TopCenter:
r = new Rectangle(this.Width / 2 - this.ImageSize.Width / 2,8,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.TopRight:
r = new Rectangle(this.Width - 8 - this.ImageSize.Width,8,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.MiddleLeft:
r = new Rectangle(8,this.Height / 2 - this.ImageSize.Height / 2,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.MiddleCenter:
r = new Rectangle(this.Width / 2 - this.ImageSize.Width / 2,this.Height / 2 - this.ImageSize.Height / 2,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.MiddleRight:
r = new Rectangle(this.Width - 8 - this.ImageSize.Width,this.Height / 2 - this.ImageSize.Height / 2,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.BottomLeft:
r = new Rectangle(8,this.Height - 8 - this.ImageSize.Height,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.BottomCenter:
r = new Rectangle(this.Width / 2 - this.ImageSize.Width / 2,this.Height - 8 - this.ImageSize.Height,this.ImageSize.Width,this.ImageSize.Height);
break;
case ContentAlignment.BottomRight:
r = new Rectangle(this.Width - 8 - this.ImageSize.Width,this.Height - 8 - this.ImageSize.Height,this.ImageSize.Width,this.ImageSize.Height);
break;
}
g.DrawImage(this.Image,r);
}
private void SetClip(Graphics g)
{
Rectangle r = this.ClientRectangle;
r.X++; r.Y++; r.Width-=3; r.Height-=3;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
g.SetClip(rr);
}
}
#endregion
#region - Private Subs -
private void VistaButton_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
DrawBackground(e.Graphics);
DrawHighlight(e.Graphics);
DrawImage(e.Graphics);
DrawText(e.Graphics);
DrawGlow(e.Graphics);
DrawOuterStroke(e.Graphics);
DrawInnerStroke(e.Graphics);
}
private void VistaButton_Resize(object sender, EventArgs e)
{
Rectangle r = this.ClientRectangle;
r.X -= 1; r.Y -= 1;
r.Width += 2; r.Height += 2;
using (GraphicsPath rr = RoundRect(r, CornerRadius, CornerRadius, CornerRadius, CornerRadius))
{
this.Region = new Region(rr);
}
}
#region - Mouse and Keyboard Events -
private void VistaButton_MouseEnter(object sender, EventArgs e)
{
mButtonState = State.Hover;
mFadeOut.Stop();
mFadeIn.Start();
}
private void VistaButton_MouseLeave(object sender, EventArgs e)
{
mButtonState = State.None;
if (this.mButtonStyle == Style.Flat) { mGlowAlpha = 0; }
mFadeIn.Stop();
mFadeOut.Start();
}
private void VistaButton_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
mButtonState = State.Pressed;
if (this.mButtonStyle != Style.Flat) { mGlowAlpha = 255; }
mFadeIn.Stop();
mFadeOut.Stop();
this.Invalidate();
}
}
private void mFadeIn_Tick(object sender, EventArgs e)
{
if (this.ButtonStyle == Style.Flat) {mGlowAlpha = 0;}
if (mGlowAlpha + 30 >= 255)
{
mGlowAlpha = 255;
mFadeIn.Stop();
}
else
{
mGlowAlpha += 30;
}
this.Invalidate();
}
private void mFadeOut_Tick(object sender, EventArgs e)
{
if (this.ButtonStyle == Style.Flat) {mGlowAlpha = 0;}
if (mGlowAlpha - 30 <= 0)
{
mGlowAlpha = 0;
mFadeOut.Stop();
}
else
{
mGlowAlpha -= 30;
}
this.Invalidate();
}
private void VistaButton_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
MouseEventArgs m = new MouseEventArgs(MouseButtons.Left,0,0,0,0);
VistaButton_MouseDown(sender, m);
}
}
private void VistaButton_KeyUp(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Space)
{
MouseEventArgs m = new MouseEventArgs(MouseButtons.Left,0,0,0,0);
calledbykey = true;
VistaButton_MouseUp(sender, m);
}
}
private void VistaButton_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
mButtonState = State.Hover;
mFadeIn.Stop();
mFadeOut.Stop();
this.Invalidate();
if (calledbykey == true) {this.OnClick(EventArgs.Empty); calledbykey = false;}
}
}
#endregion
#endregion
}
Before anybody points it out I know that a there is a question with the same title that has already been asked here it just doesn't answer my issue I think.
Working in .NET 3.5 As in that question I am making an area selection component to select an area on a picture. The picture is displayed using a custom control in which the picture is drawn during OnPaint.
I have the following code for my selection rectangle:
internal class AreaSelection : Control
{
private Rectangle selection
{
get { return new Rectangle(Point.Empty, Size.Subtract(this.Size, new Size(1, 1))); }
}
private Size mouseStartLocation;
public AreaSelection()
{
this.Size = new Size(150, 150);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.FromArgb(70, 200, 200, 200);
}
protected override void OnMouseEnter(EventArgs e)
{
this.Cursor = Cursors.SizeAll;
base.OnMouseEnter(e);
}
protected override void OnMouseDown(MouseEventArgs e)
{
this.mouseStartLocation = new Size(e.Location);
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Point offset = e.Location - this.mouseStartLocation;
this.Left += offset.X;
this.Top += offset.Y;
}
base.OnMouseMove(e);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawRectangle(new Pen(Color.Black) { DashStyle = DashStyle.Dash }, this.selection);
Debug.WriteLine("Selection redrawn");
}
}
Which gives me a nice semi-transparent rectangle which I can drag around. The problem I have is that whilst dragging the underlying image which shows through the rectangle gets lags behind the position of the rectangle.
This gets more noticeable the faster I move the rectangle. When I stop moving it the image catches up and everything aligns perfectly again.
I assume that there is something wrong with the way the rectangle draws, but I really can't figure out what it is...
Any help would be much appreciated.
EDIT:
I have noticed that the viewer gets redrawn twice as often as the selection area when I drag the selection area. Could this be the cause of the problem?
EDIT 2:
Here is the code for the viewer in case it is relevant:
public enum ImageViewerViewMode
{
Normal,
PrintSelection,
PrintPreview
}
public enum ImageViewerZoomMode
{
None,
OnClick,
Lens
}
public partial class ImageViewer : UserControl
{
/// <summary>
/// The current zoom factor. Note: Use SetZoom() to set the value.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public float ZoomFactor
{
get { return this.zoomFactor; }
private set
{
this.zoomFactor = value;
}
}
/// <summary>
/// The maximum zoom factor to use
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public float MaximumZoomFactor
{
get
{
return this.maximumZoomFactor;
}
set
{
this.maximumZoomFactor = value;
this.SetZoomFactorLimits();
}
}
/// <summary>
/// The minimum zoom factort to use
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public float MinimumZoomFactor
{
get
{
return this.minimumZoomFactor;
}
set
{
this.minimumZoomFactor = value;
this.SetZoomFactorLimits();
}
}
/// <summary>
/// The multiplying factor to apply to each ZoomIn/ZoomOut command
/// </summary>
[Category("Behavior")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[DefaultValue(2F)]
public float ZoomStep { get; set; }
/// <summary>
/// The image currently displayed by the control
/// </summary>
[Category("Data")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public Image Image
{
get { return this.image; }
set
{
this.image = value;
this.ZoomExtents();
this.minimumZoomFactor = this.zoomFactor / 10;
this.MaximumZoomFactor = this.zoomFactor * 10;
}
}
public ImageViewerViewMode ViewMode { get; set; }
public ImageViewerZoomMode ZoomMode { get; set; }
private ImageViewerLens Lens { get; set; }
private float zoomFactor;
private float minimumZoomFactor;
private float maximumZoomFactor;
private bool panning;
private Point imageLocation;
private Point imageTranslation;
private Image image;
private AreaSelection areaSelection;
/// <summary>
/// Class constructor
/// </summary>
public ImageViewer()
{
this.DoubleBuffered = true;
this.MinimumZoomFactor = 0.1F;
this.MaximumZoomFactor = 10F;
this.ZoomStep = 2F;
this.UseScannerUI = true;
this.Lens = new ImageViewerLens();
this.ViewMode = ImageViewerViewMode.PrintSelection;
this.areaSelection = new AreaSelection();
this.Controls.Add(this.areaSelection);
// TWAIN
// Initialise twain
this.twain = new Twain(new WinFormsWindowMessageHook(this));
// Try to set the last used default scanner
if (this.AvailableScanners.Any())
{
this.twain.TransferImage += twain_TransferImage;
this.twain.ScanningComplete += twain_ScanningComplete;
if (!this.SetScanner(this.defaultScanner))
this.SetScanner(this.AvailableScanners.First());
}
}
/// <summary>
/// Saves the currently loaded image under the specified filename, in the specified format at the specified quality
/// </summary>
/// <param name="FileName">The file name (full file path) under which to save the file. File type extension is not required.</param>
/// <param name="Format">The file format under which to save the file</param>
/// <param name="Quality">The quality in percent of the image to save. This is optional and may or may not be used have an effect depending on the chosen file type. Default is maximum quality.</param>
public void SaveImage(string FileName, GraphicFormats Format, uint Quality = 100)
{
ImageCodecInfo encoder;
EncoderParameters encoderParameters;
if (FileName.IsNullOrEmpty())
throw new ArgumentNullException(FileName);
else
{
string extension = Path.GetExtension(FileName);
if (!string.IsNullOrEmpty(extension))
FileName = FileName.Replace(extension, string.Empty);
FileName += "." + Format.ToString();
}
Quality = Math.Min(Math.Max(1, Quality), 100);
if (!TryGetEncoder(Format, out encoder))
return;
encoderParameters = new EncoderParameters(1);
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, (int)Quality);
this.Image.Save(FileName, encoder, encoderParameters);
}
/// <summary>
/// Tries to retrieve the appropriate encoder for the chose image format.
/// </summary>
/// <param name="Format">The image format for which to attempt retrieving the encoder</param>
/// <param name="Encoder">The encoder object in which to store the encoder if found</param>
/// <returns>True if the encoder was found, else false</returns>
private bool TryGetEncoder(GraphicFormats Format, out ImageCodecInfo Encoder)
{
ImageCodecInfo[] codecs;
codecs = ImageCodecInfo.GetImageEncoders();
Encoder = codecs.First(c => c.FormatDescription.Equals(Format.ToString(), StringComparison.CurrentCultureIgnoreCase));
return Encoder != null;
}
/// <summary>
/// Set the zoom level to view the entire image in the control
/// </summary>
public void ZoomExtents()
{
if (this.Image == null)
return;
this.ZoomFactor = (float)Math.Min((double)this.Width / this.Image.Width, (double)this.Height / this.Image.Height);
this.LimitBasePoint(imageLocation.X, imageLocation.Y);
this.Invalidate();
}
/// <summary>
/// Multiply the zoom
/// </summary>
/// <param name="NewZoomFactor">The zoom factor to set for the image</param>
public void SetZoom(float NewZoomFactor)
{
this.SetZoom(NewZoomFactor, Point.Empty);
}
/// <summary>
/// Multiply the zoom
/// </summary>
/// <param name="NewZoomFactor">The zoom factor to set for the image</param>
/// <param name="ZoomLocation">The point in which to zoom in</param>
public void SetZoom(float NewZoomFactor, Point ZoomLocation)
{
int x;
int y;
float multiplier;
multiplier = NewZoomFactor / this.ZoomFactor;
x = (int)((ZoomLocation.IsEmpty ? this.Width / 2 : ZoomLocation.X - imageLocation.X) / ZoomFactor);
y = (int)((ZoomLocation.IsEmpty ? this.Height / 2 : ZoomLocation.Y - imageLocation.Y) / ZoomFactor);
if ((multiplier < 1 && this.ZoomFactor > this.MinimumZoomFactor) || (multiplier > 1 && this.ZoomFactor < this.MaximumZoomFactor))
ZoomFactor *= multiplier;
else
return;
LimitBasePoint((int)(this.Width / 2 - x * ZoomFactor), (int)(this.Height / 2 - y * ZoomFactor));
this.Invalidate();
}
/// <summary>
/// Determines the base point for positioning the image
/// </summary>
/// <param name="x">The x coordinate based on which to determine the positioning</param>
/// <param name="y">The y coordinate based on which to determine the positioning</param>
private void LimitBasePoint(int x, int y)
{
int width;
int height;
if (this.Image == null)
return;
width = this.Width - (int)(Image.Width * ZoomFactor);
height = this.Height - (int)(Image.Height * ZoomFactor);
x = width < 0 ? Math.Max(Math.Min(x, 0), width) : width / 2;
y = height < 0 ? Math.Max(Math.Min(y, 0), height) : height / 2;
imageLocation = new Point(x, y);
}
/// <summary>
/// Verify that the maximum and minimum zoom are correctly set
/// </summary>
private void SetZoomFactorLimits()
{
float maximum = this.MaximumZoomFactor;
float minimum = this.minimumZoomFactor;
this.maximumZoomFactor = Math.Max(maximum, minimum);
this.minimumZoomFactor = Math.Min(maximum, minimum);
}
/// <summary>
/// Mouse button down event
/// </summary>
protected override void OnMouseDown(MouseEventArgs e)
{
switch (this.ZoomMode)
{
case ImageViewerZoomMode.OnClick:
switch (e.Button)
{
case MouseButtons.Left:
this.SetZoom(this.ZoomFactor * this.ZoomStep, e.Location);
break;
case MouseButtons.Middle:
this.panning = true;
this.Cursor = Cursors.NoMove2D;
this.imageTranslation = e.Location;
break;
case MouseButtons.Right:
this.SetZoom(this.ZoomFactor / this.ZoomStep, e.Location);
break;
}
break;
case ImageViewerZoomMode.Lens:
if (e.Button == MouseButtons.Left)
{
this.Cursor = Cursors.Cross;
this.Lens.Location = e.Location;
this.Lens.Visible = true;
}
else
{
this.Cursor = Cursors.Default;
this.Lens.Visible = false;
}
this.Invalidate();
break;
}
base.OnMouseDown(e);
}
/// <summary>
/// Mouse button up event
/// </summary>
protected override void OnMouseUp(MouseEventArgs e)
{
switch (this.ZoomMode)
{
case ImageViewerZoomMode.OnClick:
if (e.Button == MouseButtons.Middle)
{
panning = false;
this.Cursor = Cursors.Default;
}
break;
case ImageViewerZoomMode.Lens:
break;
}
base.OnMouseUp(e);
}
/// <summary>
/// Mouse move event
/// </summary>
protected override void OnMouseMove(MouseEventArgs e)
{
switch (this.ViewMode)
{
case ImageViewerViewMode.Normal:
switch (this.ZoomMode)
{
case ImageViewerZoomMode.OnClick:
if (panning)
{
LimitBasePoint(imageLocation.X + e.X - this.imageTranslation.X, imageLocation.Y + e.Y - this.imageTranslation.Y);
this.imageTranslation = e.Location;
}
break;
case ImageViewerZoomMode.Lens:
if (this.Lens.Visible)
{
this.Lens.Location = e.Location;
}
break;
}
break;
case ImageViewerViewMode.PrintSelection:
break;
case ImageViewerViewMode.PrintPreview:
break;
}
base.OnMouseMove(e);
}
/// <summary>
/// Resize event
/// </summary>
protected override void OnResize(EventArgs e)
{
LimitBasePoint(imageLocation.X, imageLocation.Y);
this.Invalidate();
base.OnResize(e);
}
/// <summary>
/// Paint event
/// </summary>
protected override void OnPaint(PaintEventArgs pe)
{
Rectangle src;
Rectangle dst;
pe.Graphics.Clear(this.BackColor);
if (this.Image != null)
{
switch (this.ViewMode)
{
case ImageViewerViewMode.Normal:
src = new Rectangle(Point.Empty, new Size(Image.Width, Image.Height));
dst = new Rectangle(this.imageLocation, new Size((int)(this.Image.Width * this.ZoomFactor), (int)(this.Image.Height * this.ZoomFactor)));
pe.Graphics.DrawImage(this.Image, dst, src, GraphicsUnit.Pixel);
this.Lens.Draw(pe.Graphics, this.Image, this.ZoomFactor, this.imageLocation);
break;
case ImageViewerViewMode.PrintSelection:
src = new Rectangle(Point.Empty, new Size(Image.Width, Image.Height));
dst = new Rectangle(this.imageLocation, new Size((int)(this.Image.Width * this.ZoomFactor), (int)(this.Image.Height * this.ZoomFactor)));
pe.Graphics.DrawImage(this.Image, dst, src, GraphicsUnit.Pixel);
break;
case ImageViewerViewMode.PrintPreview:
break;
}
}
//Debug.WriteLine("Viewer redrawn " + DateTime.Now);
base.OnPaint(pe);
}
}
EDIT 3:
Experience further graphics-related trouble when setting the height to something large. For example, if in the AreaSelection constructor I set the height to 500, dragging the control really screws up the painting.
whilst dragging the underlying image which shows through the rectangle gets lags behind
This is rather inevitable, updating the rectangle also redraws the image. And if that's expensive, say more than 30 milliseconds, then this can become noticeable to the eye.
That's a lot of milliseconds for something as simple as an image on a modern machine. The only way it can take that long is when the image is large and needs to be rescaled to fit the picturebox. And the pixel format is incompatible with the pixel format of the video adapter so that every single one of them has to be translated from the image pixel format to the video adapter's pixel format. That can indeed add up to multiple milliseconds.
You'll need to help to avoid PictureBox from having to burn that many cpu cycles every time the image gets painted. Do so by prescaling the image, turning it from a huge bitmap into one that better fits the control. And by altering the pixel format, the 32bppPArgb format is best by a long shot since that matches the pixel format of the vast majority of all video adapters. It draws ten times faster than all the other formats. You'll find boilerplate code to make this conversion in this answer.