I am writing a program that displays a map, and on top of it another layer where position of cameras and their viewing direction is shown. The map itself can be zoomed and panned. The problem is that the map files are of significant size and zooming does not go smoothly.
I created class ZoomablePictureBox : PictureBox to add zooming and panning ability. I tried different methods, from this and other forums, for zooming and panning and ended up with the following, firing on the OnPaint event of ZoomablePictureBox:
private void DrawImgZoomed(PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
if (imgZoomed != null)
e.Graphics.DrawImage(imgZoomed, new Rectangle(-ShiftX, -ShiftY, imgZoomed.Width, imgZoomed.Height), 0, 0, imgZoomed.Width, imgZoomed.Height, GraphicsUnit.Pixel);
}
Where ShiftX and ShiftY provide proper map panning (calculation irrelevant for this problem).
imgZoomed is zoomed version of original map calculated in BackgroundWorker everytime the zoom changes:
private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
Bitmap workerImage = e.Argument as Bitmap;
Bitmap result;
result = new Bitmap(workerImage, new Size((int)(workerImage.Width * Zoom), (int)(workerImage.Height * Zoom)));
e.Result = result;
}
So current approach is, that everytime user scrolls mousewheel, the new imgZoomed is calculated based on current Zoom. With map size of ~30 MB this can take up to 0,5 second which is annoying, but panning runs smoothly.
I realize that this may not be the best idea. In previous approach i did not create zoomed image copy everytime mouse is scrolled but did this instead:
e.Graphics.DrawImage(Image, new Rectangle(-ShiftX, -ShiftY, (int)(this._image.Width * Zoom), (int)(this._image.Height * Zoom)), 0, 0, Image.Width, Image.Height, GraphicsUnit.Pixel);
Zoom was much smoother, because from what i understand, it just stretched original image. On the other hand panning was skipping heavily.
I wast thinking of:
creating copies of orignial map for each zoom in memory/on hard drive - it will take up too much memory/hdd space
creating copies of orignial map for next/actual/previous zooms so i
have more time to calculate next step - it will not help if user
scrolls more than one step at a time
I also tried matrix transformations - no real performance gain, and calculating pan was a real pain in the arse.
I'm running in circles here and don't know how to do it. If i open the map in default Windows picture viewer zooming and panning is smooth. How do they do that?
In what way do I achieve smooth zooming and panning at the same time?
Sorry this is so long, I left in only the essential parts.
Most of the p/invoke stuff is taken from pinvoke.net
My solution for smooth panning across large images involves using the StretchBlt gdi method instead of Graphics.DrawImage. I've created a static class that groups all the native blitting operations.
Another thing that helps greatly is caching the HBitmap and Graphics objects of the Bitmap.
public class ZoomPanWindow
{
private Bitmap map;
private Graphics bmpGfx;
private IntPtr hBitmap;
public Bitmap Map
{
get { return map; }
set
{
if (map != value)
{
map = value;
//dispose/delete any previous caches
if (bmpGfx != null) bmpGfx.Dispose();
if (hBitmap != null) StretchBltHelper.DeleteObject(hBitmap);
if (value == null) return;
//cache the new HBitmap and Graphics.
bmpGfx = Graphics.FromImage(map);
hBitmap = map.GetHbitmap();
}
}
}
protected override void OnPaint(PaintEventArgs e)
{
if (map == null) return;
//finally, the actual painting!
Rectangle mapRect = //whatever zoom/pan logic you implemented.
Rectangle thisRect = new Rectangle(0, 0, this.Width, this.Height);
StretchBltHelper.DrawStretch(
hBitmap,
bmpGfx,
e.Graphics,
mapRect,
thisRect);
}
}
public static class StretchBltHelper
{
public static void DrawStretch(IntPtr hBitmap, Graphics srcGfx, Graphics destGfx,
Rectangle srcRect, Rectangle destRect)
{
IntPtr pTarget = destGfx.GetHdc();
IntPtr pSource = CreateCompatibleDC(pTarget);
IntPtr pOrig = SelectObject(pSource, hBitmap);
if (!StretchBlt(pTarget, destRect.X, destRect.Y, destRect.Width, destRect.Height,
pSource, srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
TernaryRasterOperations.SRCCOPY))
throw new Win32Exception(Marshal.GetLastWin32Error());
IntPtr pNew = SelectObject(pSource, pOrig);
DeleteDC(pSource);
destGfx.ReleaseHdc(pTarget);
}
[DllImport("gdi32.dll", EntryPoint = "SelectObject")]
public static extern System.IntPtr SelectObject(
[In()] System.IntPtr hdc,
[In()] System.IntPtr h);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject(
[In()] System.IntPtr ho);
[DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
static extern IntPtr CreateCompatibleDC(IntPtr hdc);
[DllImport("gdi32.dll")]
static extern bool StretchBlt(IntPtr hdcDest, int nXOriginDest, int nYOriginDest,
int nWidthDest, int nHeightDest,
IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc,
TernaryRasterOperations dwRop);
public enum TernaryRasterOperations : uint
{
SRCCOPY = 0x00CC0020
//there are many others but we don't need them for this purpose, omitted for brevity
}
}
Related
I have searched for hours and tried many things.
I have a transparent Windows Form and I am trying to capture the screen within the bounds of that form.
For whatever reason, I am unable to get the correct form area...
[DllImport("user32.dll")]
public static extern IntPtr GetDC(IntPtr ptr);
[DllImport("user32.dll")]
public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
private void LocationChanged(object sender, EventArgs e)
{
var position = form.PointToScreen(Point.Empty);
bounds = new Rectangle(position, new Size(50, 50));
DrawRectangle(bounds);
}
void DrawRectangle(Rectangle rect)
{
IntPtr desktopPtr = GetDC(IntPtr.Zero);
Graphics g = Graphics.FromHdc(desktopPtr);
SolidBrush b = new SolidBrush(Color.Green);
g.FillRectangle(b, rect);
g.Dispose();
ReleaseDC(IntPtr.Zero, desktopPtr);
}
This is the result:
The green square should completely cover the form with the red border on the left.
I've tried different monitors, I've made sure scaling is 100% (what to do if it is not?), I'e tried passing the form.Location to form.PointToScreen
What am I doing wrong?
You can try setting the property form.StartPosition = FormPosition.Manual
https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.form.startposition?view=netframework-4.7.2
I'm trying to build a map editor like this one: http://blog.rpgmakerweb.com/wp-content/uploads/2012/04/T2EditorLabelled.png in WPF.
How should it work:
By selecting a specific tile on the "tile list" - B section - is possible to draw that tile on the canvas - section A.
In the end the final result is a full game level drawn on the canvas.
First approach:
In my first approach each tile is drawn by creating a new image control and adding it to the canvas (a WPF canvas control).
Steps:
Select the tile from the tileset
Catch the click event on the canvas
Create a new image, cropping the tile from the tileset
Adding the image in the right spot on the canvas (as a child)
This method is quite naive and it imply two big issues:
All the pixels are already buffered on the tileset that contains all the tiles, but each time I draw a tile on the canvas a new image is create and by doing so I am obliged to replicate part of the tileset pixel data as a source for the new
Too many controls on the canvas:
A map game can reach the size of 1000 tiles x 1000 tiles, WPF start to have sensible decay of performance on a 100x100 tile map.
So create a control image for each tile is an unfeasible solution.
Second approach:
My second approach contemplate the use of a single big WriteableBitmap as canvas background.
As in the previous approach the tile is selected on the tileset and the draw event is a click on the canvas.
In this case though no new image is created ex-novo but the background WriteableBitmap is modified accordingly.
So in the number of controls is reduced drastically since all the draw mechanism is performed on the WriteableBitmap.
The main problem with this approach is that if I want to create a large map with 1k x 1k tiles with a tile of 32x32, the size of the background image would be astronomical.
I wonder if there is a way to develop a good solution of this problem in WPF.
How would you address this development problem?
There a variety of different ways you could approach this problem to increase performance.
In terms of the rendering of images, WPF by default isn't amazing, so you could;
Use GDI's BitBlt to quickly render images to a WinForms control which can be hosted. The benefit of this is that GDI is software and hence does not require a graphics card or anything. (WPF fast method to draw image in UI)
You can use D3DImage as an image source. This would mean that you can use the D3DImage as the canvas to which to draw. Doing this would mean you would have to render all the tiles to the D3DImage image source using Direct3D, which would be much faster as it is hardware accelerated. (https://www.codeproject.com/Articles/28526/Introduction-to-D3DImage)
You may be able to host XNA through a WinForms control and rendering using that, I have no experience with this, so I test to any performance. (WPF vs XNA to render thousands of sprites)
Personally, for rendering, I would use the GDI method as it is software based, relatively easy to set up and I have had experience with it, and seen it's performance.
Additionally, when rendering the tiles to a control, you can use the scrollbar positions and control size to determine the area of your map which is actually visible. From this, you can simply select those few tiles and only render them, saving a lot of time.
Furthermore, when you manage it yourself, you can simply load the different sprites into memory and then use that same memory to draw it in different locations onto a buffer image. This would decrease that memory issue you mentioned.
Below is my example code for the GDI method, I render 2500 32x32 pixel sprites (all of which are the same green color, however you'd set this memory to an actual sprite - src memory). The sprites are bitblit to a buffer image (srcb memory) which is then bitblit to the window, in your case, you'd bitblit the buffer image to a winforms canvas or something. With this, I get between 30 and 40 fps on my base model Surface Pro 3. This should be enough performance for a level editor's rendering. Please note this code is very crude and just roughly outlines the process, it can almost certainly be improved upon.
//
// GDI DLL IMPORT
//
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteDC(IntPtr hDC);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern bool BitBlt(IntPtr hDC, int x, int y, int width, int height, IntPtr hDCSource, int sourceX, int sourceY, uint type);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern bool FillRgn(IntPtr hdc, IntPtr hrgn, IntPtr hbr);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern IntPtr CreateSolidBrush(uint crColor);
[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int nWidth, int nHeight);
public const uint SRCCOPY = 0x00CC0020; // dest = source
public const uint SRCPAINT = 0x00EE0086; // dest = source OR dest
public const uint SRCAND = 0x008800C6; // dest = source AND dest
public const uint SRCINVERT = 0x00660046; // dest = source XOR dest
public const uint SRCERASE = 0x00440328; // dest = source AND (NOT dest )
public const uint NOTSRCCOPY = 0x00330008; // dest = (NOT source)
public const uint NOTSRCERASE = 0x001100A6; // dest = (NOT src) AND (NOT dest)
public const uint MERGECOPY = 0x00C000CA; // dest = (source AND pattern)
public const uint MERGEPAINT = 0x00BB0226; // dest = (NOT source) OR dest
public const uint PATCOPY = 0x00F00021; // dest = pattern
public const uint PATPAINT = 0x00FB0A09; // dest = DPSnoo
public const uint PATINVERT = 0x005A0049; // dest = pattern XOR dest
public const uint DSTINVERT = 0x00550009; // dest = (NOT dest)
public const uint BLACKNESS = 0x00000042; // dest = BLACK
public const uint WHITENESS = 0x00FF0062; // dest = WHITE
//
// END DLL IMPORT
//
//GDI Graphics
private Graphics g;
//Colors
private const int BACKGROUND_COLOR = 0xffffff;
private const int GRAPH_COLOR_ONE = 0x00FF00;
//Pointers
IntPtr hdc;
IntPtr srcb;
IntPtr dchb;
IntPtr origb;
IntPtr src;
IntPtr dch;
IntPtr orig;
//Brushes
IntPtr brush_one;
IntPtr brush_back;
public Form1()
{
InitializeComponent();
//Create a graphics engine from the window
g = Graphics.FromHwnd(this.Handle);
//Get the handle of the Window's graphics and then create a compatible source handle
hdc = g.GetHdc();
srcb = CreateCompatibleDC(hdc);
src = CreateCompatibleDC(hdc);
//Get the handle of a new compatible bitmap object and map it using the source handle to produce a handle to the actual source
dchb = CreateCompatibleBitmap(hdc, ClientRectangle.Width, ClientRectangle.Height);
origb = SelectObject(srcb, dchb);
//Get the handle of a new compatible bitmap object and map it using the source handle to produce a handle to the actual source
dch = CreateCompatibleBitmap(hdc, 32, 32);
orig = SelectObject(src, dch);
//Create the burshes
brush_one = CreateSolidBrush(GRAPH_COLOR_ONE);
brush_back = CreateSolidBrush(BACKGROUND_COLOR);
//Create Image
FillRectangle(brush_one, src, 0, 0, 32, 32);
//Fill Background
FillRectangle(brush_back, hdc, 0, 0, ClientRectangle.Width, ClientRectangle.Height);
this.Show();
Render();
}
private void Render()
{
Stopwatch s = new Stopwatch();
s.Start();
int frames = 0;
while(frames <= 30)
{
frames++;
FillRectangle(brush_back, srcb, 0, 0, ClientRectangle.Width, ClientRectangle.Height);
for (int i = 0; i < 50; i++)
for (int j = 0; j < 50; j++)
BlitBitmap(i * 5, j * 5, 32, 32, srcb, src);
BlitBitmap(0, 0, ClientRectangle.Width, ClientRectangle.Height, hdc, srcb);
}
s.Stop();
float fps = (float)frames / ((float)s.ElapsedMilliseconds / 1000.0f);
MessageBox.Show(Math.Round(fps, 2).ToString(), "FPS");
}
private void FillRectangle(IntPtr b, IntPtr hdc, int x, int y, int w, int h)
{
//Create the region
IntPtr r = CreateRectRgn(x, y, x + w, y + h);
//Fill the region using the specified brush
FillRgn(hdc, r, b);
//Delete the region object
DeleteObject(r);
}
private void BlitBitmap(int x, int y, int w, int h, IntPtr to, IntPtr from)
{
//Blit the bits of the actual source object to the window, using its handle
BitBlt(to, x, y, w, h, from, 0, 0, SRCCOPY);
}
I actually wanted to Convert RTF into Image so after googling a lot I've got a code that does it by Paint() Event of Picturebox1 and it works perfectly :
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(richTextBox1.BackColor);
e.Graphics.DrawRtfText(this.richTextBox1.Rtf, this.pictureBox1.ClientRectangle);
base.OnPaint(e);
// below code just create an empty image file
Bitmap newBitmap = new Bitmap(pictureBox1.Width, pictureBox1.Height);
e.Graphics.DrawImage(newBitmap, new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height), new Rectangle(0, 0, pictureBox1.Width, pictureBox1.Height), GraphicsUnit.Pixel);
newBitmap.Save(#"c:\adv.jpg");
}
in the picture above the left is my richTextBox and the right is a Picturebox.
the ISSUE is I don't know how to save Paint() drew graphic into a file because the 3 last lines of my code just save an empty image.
UPDATE #1:
g.SmoothingMode = SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.Clear(richTextBox1.BackColor);
g.DrawRtfText(this.richTextBox1.Rtf, this.pictureBox1.ClientRectangle);
by changing the graphics from e.graphics to g the issue is resolved but with one other issue that the quality of bitmap is too low. I've Added this bunch of code but I've got same result, the quality is too low!
Any suggestions?
UPDATE #2
here is the Graphics_DrawRtfText class that does the conversion :
public static class Graphics_DrawRtfText
{
private static RichTextBoxDrawer rtfDrawer;
public static void DrawRtfText(this Graphics graphics, string rtf, Rectangle layoutArea)
{
if (Graphics_DrawRtfText.rtfDrawer == null)
{
Graphics_DrawRtfText.rtfDrawer = new RichTextBoxDrawer();
}
Graphics_DrawRtfText.rtfDrawer.Rtf = rtf;
Graphics_DrawRtfText.rtfDrawer.Draw(graphics, layoutArea);
}
private class RichTextBoxDrawer : RichTextBox
{
//Code converted from code found here: http://support.microsoft.com/kb/812425/en-us
//Convert the unit used by the .NET framework (1/100 inch)
//and the unit used by Win32 API calls (twips 1/1440 inch)
private const double anInch = 14.4;
protected override CreateParams CreateParams
{
get
{
CreateParams createParams = base.CreateParams;
if (SafeNativeMethods.LoadLibrary("msftedit.dll") != IntPtr.Zero)
{
createParams.ExStyle |= SafeNativeMethods.WS_EX_TRANSPARENT; // transparent
createParams.ClassName = "RICHEDIT50W";
}
return createParams;
}
}
public void Draw(Graphics graphics, Rectangle layoutArea)
{
//Calculate the area to render.
SafeNativeMethods.RECT rectLayoutArea;
rectLayoutArea.Top = (int)(layoutArea.Top * anInch);
rectLayoutArea.Bottom = (int)(layoutArea.Bottom * anInch);
rectLayoutArea.Left = (int)(layoutArea.Left * anInch);
rectLayoutArea.Right = (int)(layoutArea.Right * anInch);
IntPtr hdc = graphics.GetHdc();
SafeNativeMethods.FORMATRANGE fmtRange;
fmtRange.chrg.cpMax = -1; //Indicate character from to character to
fmtRange.chrg.cpMin = 0;
fmtRange.hdc = hdc; //Use the same DC for measuring and rendering
fmtRange.hdcTarget = hdc; //Point at printer hDC
fmtRange.rc = rectLayoutArea; //Indicate the area on page to print
fmtRange.rcPage = rectLayoutArea; //Indicate size of page
IntPtr wParam = IntPtr.Zero;
wParam = new IntPtr(1);
//Get the pointer to the FORMATRANGE structure in memory
IntPtr lParam = IntPtr.Zero;
lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fmtRange));
Marshal.StructureToPtr(fmtRange, lParam, false);
SafeNativeMethods.SendMessage(this.Handle, SafeNativeMethods.EM_FORMATRANGE, wParam, lParam);
//Free the block of memory allocated
Marshal.FreeCoTaskMem(lParam);
//Release the device context handle obtained by a previous call
graphics.ReleaseHdc(hdc);
}
#region SafeNativeMethods
private static class SafeNativeMethods
{
[DllImport("USER32.dll")]
public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr LoadLibrary(string lpFileName);
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
public struct CHARRANGE
{
public int cpMin; //First character of range (0 for start of doc)
public int cpMax; //Last character of range (-1 for end of doc)
}
[StructLayout(LayoutKind.Sequential)]
public struct FORMATRANGE
{
public IntPtr hdc; //Actual DC to draw on
public IntPtr hdcTarget; //Target DC for determining text formatting
public RECT rc; //Region of the DC to draw to (in twips)
public RECT rcPage; //Region of the whole DC (page size) (in twips)
public CHARRANGE chrg; //Range of text to draw (see earlier declaration)
}
public const int WM_USER = 0x0400;
public const int EM_FORMATRANGE = WM_USER + 57;
public const int WS_EX_TRANSPARENT = 0x20;
}
#endregion
}
}
Disclaimer: I don't have the time to dig into the posted extension method but it is interesting and works well, at least when drawing onto a control surface.
But I could reproduce how bad the results are when drawing into a bitmap..
But: When done right the saved results are excellent!
So here here are a few things to keep in mind:
Saving in the Paint event is a bad idea, as this event will be triggered by the system whenever it needs to redraw the control; test by doing a minimize/maximize cycle.
In addition the DrawRtfText semms to create a double-vision effect when drawing into a bitmap.
So make sure you use DrawToBitmap to grab the results. For this you need to place the call to DrawRtfText in the Paint event of a control!
Also make sure to have large enough resolutions both in the control (pixel size) and the Bitmap (dpi) to get nice, crispy and (if needed) printable results.
Do not save to jpg as this is bound to result in blurry text! Png is the format of choice!
Here is a Paint event:
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(richTextBox1.BackColor);
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
Padding pad = new Padding(120, 230, 10, 30); // pick your own numbers!
Size sz = panel1.ClientSize;
Rectangle rect = new Rectangle(pad.Left, pad.Top,
sz.Width - pad.Horizontal, sz.Height - pad.Vertical);
e.Graphics.DrawRtfText(this.richTextBox1.Rtf, rect);
}
Note that it pays to improve on the default quality settings; if you don't the text in the resulting file will break apart when zooming in..
Here is a Save button click:
private void button1_Click(object sender, EventArgs e)
{
Size sz = panel1.ClientSize;
// first we (optionally) create a bitmap in the original panel size:
Rectangle rect1 = panel1.ClientRectangle;
Bitmap bmp = new Bitmap(rect1.Width, rect1.Height);
panel1.DrawToBitmap(bmp, rect);
bmp.Save("D:\\rtfImage1.png", ImageFormat.Png);
// now we create a 4x larger one:
Rectangle rect2 = new Rectangle(0, 0, sz.Width * 4, sz.Height * 4);
Bitmap bmp2 = new Bitmap(rect2.Width, rect2.Height);
// we need to temporarily enlarge the panel:
panel1.ClientSize = rect2.Size;
// now we can let the routine draw
panel1.DrawToBitmap(bmp2, rect2);
// and before saving we optionally can set the dpi resolution
bmp2.SetResolution(300, 300);
// optionally make background transparent:
bmp2.MakeTransparent(richTextBox1.BackColor);
UnSemi(bmp2); // see the link in the comment!
// save text always as png; jpg is only for fotos!
bmp2.Save("D:\\rtfImage2.png", ImageFormat.Png);
// restore the panels size
panel1.ClientSize = sz;
}
I found the result to be really good.
Note that DrawToBitmap will internally trigger the Paint event to grab the drawn graphics.
Of course you don't need both parts - use only the one you want (.e. skip the 1st part, between first and now ) and do use your own numbers. It helps to know what the output shall be and calculate the necessary sizes and resolutions backward from there.
I added the enlarged version because usually the monitor resolution, which is what the controls all have, is rather limited, around 75-100dpi, while print quality starts only at 150dpi..
Here is a link to the UnSemi function
Your code produces an empty image file because you are not drawing anything onto 'newBitmap'.
If you want to draw anything onto 'newBitmap' you need to create a Graphics object from it. As I do not know where 'DrawRtfText' comes from and how it works my guess would be:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
//...
Bitmap newBitmap = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics g = Graphics.FromImage(newBitmap);
g.DrawRtfText(this.richTextBox1.Rtf, this.pictureBox1.ClientRectangle);
newBitmap.Save(#"d:\adv.jpg");
}
I'm currently simulating the windows multiple selection rectangle when the user is dragging the mouse. To synchronize our understanding, this picture shows the effect I want to simulate:
Now I want to simulate this effect on a FlowLayoutPanel with some controls inside.
So far I am managed to get the effect almost done:
What I did here was putting a unfocused border-less semi-transparent (half the opacity) form on top the main form. To get the border simulated, I handled SizeChanged and Paint to draw the border.
However, this solution sometimes flickers, as in the owner border couldn't get cleared on-time:
I have tried using double buffering on the cover form by setting DoubleBuffer to true, and override CreateParam to set WM_EX_COMPOSITED, but neither works.
My question is: How to reduce this artifact?
Thanks a lot!
My code:
For the cover form:
public partial class CoverForm : Form
{
public CoverForm()
{
InitializeComponent();
BackColor = Color.CadetBlue;
FormBorderStyle = FormBorderStyle.None;
SizeChanged += (s, e) => Invalidate();
Paint += (s, e) =>
{
e.Graphics.Clear(BackColor);
using (var pen = new Pen(Color.DodgerBlue))
{
e.Graphics.DrawRectangle(pen, 1, 1, Size.Width - 2, Size.Height - 2);
}
};
}
protected override bool ShowWithoutActivation
{
get { return true; }
}
}
For the main form:
public Form1()
{
InitializeComponent();
// mainPanel is the panel that simulates the dragging effect
mainPanel.MouseDown += (s, e) =>
{
_isMouseDown = true;
_startPosition = e.Location;
coverForm.Location = mainPanel.PointToScreen(e.Location);
coverForm.Show();
};
mainPanel.MouseUp += (s, e) =>
{
_isMouseDown = false;
coverForm.Hide();
};
mainPanel.MouseMove += CoverPanelMouseMoveHandler;
DoubleBuffered = true;
}
~Form1()
{
if (coverForm != null && !coverForm.IsDisposed)
{
coverForm.Dispose();
}
}
# region Dragging Effect
private void CoverPanelMouseMoveHandler(object sender, MouseEventArgs e)
{
if (_isMouseDown)
{
_curPosition = e.Location;
// find the dragging rectangle
var rect = CreateRect(_curPosition, _startPosition);
coverForm.Size = rect.Size;
coverForm.Location = mainPanel.PointToScreen(rect.Location);
foreach (Control control in mainPanel.Controls)
{
// logic to get button backcolor changed
}
mainPanel.Invalidate(true);
}
}
Update
I have tried to override OnPaint and put my drawing there, but it gave even worse result: the old paints wouldn't get erased:
Code I modified for cover form:
public partial class CoverForm : Form
{
public CoverForm()
{
InitializeComponent();
BackColor = Color.CadetBlue;
FormBorderStyle = FormBorderStyle.None;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.Clear(BackColor);
using (var pen = new Pen(Color.FromArgb(255, 0, 0, 255)))
{
e.Graphics.DrawRectangle(pen, 0, 0, Size.Width - 1, Size.Height - 1);
}
}
protected override bool ShowWithoutActivation
{
get { return true; }
}
}
Update 2
Actually the problem I am facing is about drawing above a FlowLayoutPanel, not a normal Panel. The reason I put Panel before was I was seeking answer for my flickering 2-layers design. But since someone approach the problem by adding control to the panel to get it drawn above all controls, I would like to point this out: adding control to a panel would be trivial, but FlowLayoutPanel will auto-align the newly added control to the next available position, which may screw up the expected effect.
Video Demo of the Solution: Remember to Switch to 1080p
Recorded in a VM on a crappy machine. So kinda slow.
You are getting those artifacts because you're doing a combination of 3 things all at once.
The two big ones are moving the form to another location and resizing the form. It also doesn't help if the form is semi transparent :) To get a better understanding of what I mean, just open VS2013 up and resize the window very quickly (at the top-left corner, and run in random directions really fast), you will see that around the edges it can't keep up. And yes, you will get different results when you're resizing from a different position around the window (just think about it for a minute and you will figure it out).
Aybe, provided a pretty clever solution but it doesn't allow you to see through it or see if any updates to the panel....since it basically just copies the last output to a bitmap and uses that as a back buffer (much like what you assume someone might do when doing the selection thing in a paint program).
If you really want to do it with an overlay form and keep it semi-transparent then you will need to eliminate those three things if you don't want artifacts.
The code requires quite a bit of WIN32 knowledge.... lucky for you Microsoft has already done the hard part. We are going to enable per pixel transparency in your cover frame by using the PerPixelAlphaForm by Microsoft (you can google it) I will paste the code here. It basically just creates a Window with a Style of WS_EX_LAYERED. Keeps a Backbuffer which is AlphaBlended with the screen (simple huh?).
/******************************** Module Header ********************************\
Module Name: PerPixelAlphaForm.cs
Project: CSWinFormLayeredWindow
Copyright (c) Microsoft Corporation.
This source is subject to the Microsoft Public License.
See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL.
All other rights reserved.
THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
\*******************************************************************************/
#region Using directives
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
#endregion
namespace CSWinFormLayeredWindow
{
public partial class PerPixelAlphaForm : Form
{
public PerPixelAlphaForm()
{
InitializeComponent();
}
protected override CreateParams CreateParams
{
get
{
// Add the layered extended style (WS_EX_LAYERED) to this window.
CreateParams createParams = base.CreateParams;
createParams.ExStyle |= WS_EX_LAYERED;
return createParams;
}
}
/// <summary>
/// Let Windows drag this window for us (thinks its hitting the title
/// bar of the window)
/// </summary>
/// <param name="message"></param>
protected override void WndProc(ref Message message)
{
if (message.Msg == WM_NCHITTEST)
{
// Tell Windows that the user is on the title bar (caption)
message.Result = (IntPtr)HTCAPTION;
}
else
{
base.WndProc(ref message);
}
}
/// <summary>
///
/// </summary>
/// <param name="bitmap"></param>
public void SelectBitmap(Bitmap bitmap)
{
SelectBitmap(bitmap, 255);
}
/// <summary>
///
/// </summary>
/// <param name="bitmap">
///
/// </param>
/// <param name="opacity">
/// Specifies an alpha transparency value to be used on the entire source
/// bitmap. The SourceConstantAlpha value is combined with any per-pixel
/// alpha values in the source bitmap. The value ranges from 0 to 255. If
/// you set SourceConstantAlpha to 0, it is assumed that your image is
/// transparent. When you only want to use per-pixel alpha values, set
/// the SourceConstantAlpha value to 255 (opaque).
/// </param>
public void SelectBitmap(Bitmap bitmap, int opacity)
{
// Does this bitmap contain an alpha channel?
if (bitmap.PixelFormat != PixelFormat.Format32bppArgb)
{
throw new ApplicationException("The bitmap must be 32bpp with alpha-channel.");
}
// Get device contexts
IntPtr screenDc = GetDC(IntPtr.Zero);
IntPtr memDc = CreateCompatibleDC(screenDc);
IntPtr hBitmap = IntPtr.Zero;
IntPtr hOldBitmap = IntPtr.Zero;
try
{
// Get handle to the new bitmap and select it into the current
// device context.
hBitmap = bitmap.GetHbitmap(Color.FromArgb(0));
hOldBitmap = SelectObject(memDc, hBitmap);
// Set parameters for layered window update.
Size newSize = new Size(bitmap.Width, bitmap.Height);
Point sourceLocation = new Point(0, 0);
Point newLocation = new Point(this.Left, this.Top);
BLENDFUNCTION blend = new BLENDFUNCTION();
blend.BlendOp = AC_SRC_OVER;
blend.BlendFlags = 0;
blend.SourceConstantAlpha = (byte)opacity;
blend.AlphaFormat = AC_SRC_ALPHA;
// Update the window.
UpdateLayeredWindow(
this.Handle, // Handle to the layered window
screenDc, // Handle to the screen DC
ref newLocation, // New screen position of the layered window
ref newSize, // New size of the layered window
memDc, // Handle to the layered window surface DC
ref sourceLocation, // Location of the layer in the DC
0, // Color key of the layered window
ref blend, // Transparency of the layered window
ULW_ALPHA // Use blend as the blend function
);
}
finally
{
// Release device context.
ReleaseDC(IntPtr.Zero, screenDc);
if (hBitmap != IntPtr.Zero)
{
SelectObject(memDc, hOldBitmap);
DeleteObject(hBitmap);
}
DeleteDC(memDc);
}
}
#region Native Methods and Structures
const Int32 WS_EX_LAYERED = 0x80000;
const Int32 HTCAPTION = 0x02;
const Int32 WM_NCHITTEST = 0x84;
const Int32 ULW_ALPHA = 0x02;
const byte AC_SRC_OVER = 0x00;
const byte AC_SRC_ALPHA = 0x01;
[StructLayout(LayoutKind.Sequential)]
struct Point
{
public Int32 x;
public Int32 y;
public Point(Int32 x, Int32 y)
{ this.x = x; this.y = y; }
}
[StructLayout(LayoutKind.Sequential)]
struct Size
{
public Int32 cx;
public Int32 cy;
public Size(Int32 cx, Int32 cy)
{ this.cx = cx; this.cy = cy; }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ARGB
{
public byte Blue;
public byte Green;
public byte Red;
public byte Alpha;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct BLENDFUNCTION
{
public byte BlendOp;
public byte BlendFlags;
public byte SourceConstantAlpha;
public byte AlphaFormat;
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDst,
ref Point pptDst, ref Size psize, IntPtr hdcSrc, ref Point pprSrc,
Int32 crKey, ref BLENDFUNCTION pblend, Int32 dwFlags);
[DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
[DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteObject(IntPtr hObject);
#endregion
}
}
OK, that should eliminate your semi-transparent problem. Remember to get rid of the override of the WndProc (you won't need it). Set Double-Buffer to false and TopMost to true.
Now to eliminate the other two problems. I hope you thought of a way of doing it....but I will give you my solution. Always keep the PerPixelAlphaForm the size of your MainForm. Same location, Same SIZE. :) And resize the PerPixelAlphaForm's backbuffer bitmap to the same size as well. When you do it this way, all you have to do is redraw the Selection Rectangle. Why? because it overlays the entire MainForm perfectly.
So basically
`OnMouseDown` = Save initial point of mouse, show the Cover layer
`OnMouseMove` = clear the PerPixelAlphaForm bitmap, draw your rectangle
call SelectBitmap again update the form
`OnMouseUp` = hide the Cover layer (or whatever you want to do)
I personally have all this hook up to the Control-Key
To clear the PerPixelAlphaForm we need to do in a certain way. Give all values an Alpha of 0.
public void ClearBackbuffer()
{
Graphics g = Graphics.FromImage(_reference_to_your_backbuffer_);
g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;
SolidBrush sb = new SolidBrush(Color.FromArgb(0x00, 0x00, 0x00, 0x00));
g.FillRectangle(sb, this.ClientRectangle);
sb.Dispose();
g.Dispose();
}
Video Demo of the Solution: Remember to Switch to 1080p
If you need more help, let me know I can find some time to rip the code out of the larger program. But it seems to me you're the kind of person that likes tinkering with stuff :D
EDIT : using an additional PictureBox and Bitmap makes the whole thing working
The following Panel draws a rectangle without flickering:
internal sealed class MyPanel : Panel
{
private readonly PictureBox _pictureBox;
private Bitmap _bitmapContent;
private Bitmap _bitmapForeground;
private Point? _point1;
private Point? _point2;
public MyPanel()
{
DoubleBuffered = true;
_pictureBox = new PictureBox();
}
protected override void OnSizeChanged(EventArgs e)
{
if (_bitmapForeground != null) _bitmapForeground.Dispose();
_bitmapForeground = new Bitmap(Size.Width, Size.Height);
if (_bitmapContent != null) _bitmapContent.Dispose();
_bitmapContent = new Bitmap(Size.Width, Size.Height);
_pictureBox.Size = Size;
_pictureBox.Image = _bitmapForeground;
base.OnSizeChanged(e);
}
protected override void OnMouseDown(MouseEventArgs e)
{
_point1 = e.Location;
DrawToBitmap(_bitmapContent, new Rectangle(0, 0, Size.Width, Size.Height));
SetControlsVisibility(false);
Controls.Add(_pictureBox);
base.OnMouseDown(e);
}
private void SetControlsVisibility(bool visible)
{
IEnumerable<Control> ofType = Controls.OfType<Control>();
foreach (Control control in ofType)
{
control.Visible = visible;
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
Controls.Remove(_pictureBox);
SetControlsVisibility(true);
_point1 = null;
_point2 = null;
Refresh();
base.OnMouseUp(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (_point1 != null)
{
_point2 = e.Location;
if (_point1 != null && _point2 != null)
{
Point p1 = _point1.Value;
Point p2 = _point2.Value;
int x1 = p1.X;
int y1 = p1.Y;
int x2 = p2.X;
int y2 = p2.Y;
int xmin = Math.Min(x1, x2);
int ymin = Math.Min(y1, y2);
int xmax = Math.Max(x1, x2);
int ymax = Math.Max(y1, y2);
using (Graphics graphics = Graphics.FromImage(_bitmapForeground))
{
graphics.DrawImageUnscaled(_bitmapContent, 0, 0, _bitmapContent.Width, _bitmapContent.Height);
graphics.DrawRectangle(Pens.Red, new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin));
}
_pictureBox.Refresh();
}
}
base.OnMouseMove(e);
}
}
However, the rectangle will be below the controls, not sure why ...
I have one Windows Form with
this.BackColor = Color.Red
and
this.TransparencyKey = Color.Red
and there is one PictureBox (png image with transparency corners) on this form,
PictureBox.SizeMode = Normal.
Then I set SizeMode of PictureBox to StretchImage and get other result:you can see it here
(sorry, I can put one hyperlink only)
You can see red points/dots, but it is not Color.Red because it is transparency key of form.
I tried to implement transparent form, transparent control to remove these "red" points.
Anyways, I would like to ask about my last point - I tried to override "OnPaintBackground" method and when I implemented some like code below:
e.Graphics.FillRectangle(Brushes.Red, ClientRectangle);
TextureBrush brush = ImageHelper.ScaleImage(BackgroundImage, ClientRectangle.Width, ClientRectangle.Height);
e.Graphics.FillRectangle(brush, ClientRectangle);
I saved scaled BitMap to file before putting it to TextureBrush - this png scaled image doesn't contain "red" points, but they were painted on form.
Does somebody have any idea why it happens and tell me some way to solve it.
Best regards.
This happens because GDI+, which is drawing the image, doesn't know that red is becoming transparent.
Therefore, it blends the borders of the image with the red background, creating reddish (but not completely red) pixels which do not become transparent.
To solve this, you'll need to make a layered window.
EDIT:
Use the following native methods:
static class NativeMethods {
public const int LayeredWindow = 0x80000;//WS_EX_LAYERED
#region Drawing
[DllImport("User32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool UpdateLayeredWindow(IntPtr handle, IntPtr screenDc, ref Point windowLocation, ref Size windowSize, IntPtr imageDc, ref Point dcLocation, int colorKey, ref BlendFunction blendInfo, UlwType type);
[DllImport("gdi32.dll")]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("User32.dll")]
public static extern IntPtr GetDC(IntPtr hWnd);
[DllImport("User32.dll")]
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DeleteObject(IntPtr hObject);
#endregion
}
struct BlendFunction {
public byte BlendOp;
public byte BlendFlags;
public byte SourceConstantAlpha;
public byte AlphaFormat;
}
enum UlwType : int {
None = 0,
ColorKey = 0x00000001,
Alpha = 0x00000002,
Opaque = 0x00000004
}
Override the form's CreateParams:
protected override CreateParams CreateParams {
get {
CreateParams createParams = base.CreateParams;
createParams.ExStyle |= NativeMethods.LayeredWindow;
return createParams;
}
}
Call the following function in OnShown:
static Point Zero;
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults")]
void UpdateWindow() {
IntPtr screenDC = NativeMethods.GetDC(IntPtr.Zero);
IntPtr imageDC = NativeMethods.CreateCompatibleDC(screenDC);
IntPtr gdiBitmap = IntPtr.Zero;
IntPtr oldBitmap = IntPtr.Zero;
try {
gdiBitmap = image.GetHbitmap(Color.FromArgb(0)); //Get a GDI handle to the image.
oldBitmap = NativeMethods.SelectObject(imageDC, gdiBitmap); //Select the image into the DC, and cache the old bitmap.
Size size = image.Size; //Get the size and location of the form, as integers.
Point location = this.Location;
BlendFunction alphaInfo = new BlendFunction { SourceConstantAlpha = 255, AlphaFormat = 1 }; //This struct provides information about the opacity of the form.
NativeMethods.UpdateLayeredWindow(Handle, screenDC, ref location, ref size, imageDC, ref Zero, 0, ref alphaInfo, UlwType.Alpha);
} finally {
NativeMethods.ReleaseDC(IntPtr.Zero, screenDC); //Release the Screen's DC.
if (gdiBitmap != IntPtr.Zero) { //If we got a GDI bitmap,
NativeMethods.SelectObject(imageDC, oldBitmap); //Select the old bitmap into the DC
NativeMethods.DeleteObject(gdiBitmap); //Delete the GDI bitmap,
}
NativeMethods.DeleteDC(imageDC); //And delete the DC.
}
Invalidate();
}