How to draw a matrix on winform - c#

I have an empty class Box that i make a matrix of like you se below and i want to draw this matrix on winform how can i do that ?
For example if Box[i,j] have red color and i change the color of the box like thisBox[i.j].color = Color.Black så should my winform change the color of Box[i,j] to black after it draw the whole matrix.
Box[,] boxes = new Box[100, 100];
MainForm form;
Timer timer;
public Game()
{
form = new MainForm();
timer = new Timer();
}
public void Run(int size)
{
form.Paint += new PaintEventHandler(Draw);
timer.Tick += new EventHandler(TimerEventHandler);
timer.Interval = 1000 / 25;
timer.Start();
form.Visible = true;
}
private void TimerEventHandler(Object obj, EventArgs args)
{
form.Refresh();
}
private void Draw(Object obj, PaintEventArgs args)
{
}

Here's an approach to doing it (not the only way):
First, give Box a method to draw itself to a Graphics context:
class Box
{
public Color color { get; set; } = Color.Red;
// Note: Conventionally, property names begin with a capital letter.
/// <summary>
/// Draw this block to the given graphics context.
/// </summary>
/// <param name="g">The graphics context to draw to.</param>
/// <param name="x">X-coordinate of the block within the graphics context.</param>
/// <param name="y">Y-coordinate of the block within the graphics context.</param>
/// <param name="width">Width of the area in which to draw the block.</param>
/// <param name="height">Height of the area in which to draw the block.</param>
public void DrawTo(Graphics g, int x, int y, int width, int height)
{
// Fill in the color of this block:
Brush brush = new SolidBrush(color); // fill style, color etc.
g.FillRectangle(brush, x, y, width, height);
// Black outline:
Pen pen = new Pen(Color.Black); // line style, color, etc.
g.DrawRectangle(pen, x, y, width, height);
// look up the documentation of the System.Drawing.Graphics class for other methods for drawing.
}
}
Now, if you wanted to draw the matrix to a bitmap image, you could do this:
private void DrawMatrix(System.Drawing.Bitmap bm)
{
Graphics g = Graphics.FromImage(bm);
try
{
for (int y = 0; y < MatrixHeight; y++)
for (int x = 0; x < MatrixWidth; x++)
{
boxes[x, y].DrawTo(g, x * CellWidth, y * CellHeight, CellWidth, CellHeight);
}
}
finally
{
g.Dispose();
}
}
// Size of the matrix:
const int MatrixWidth = 100;
const int MatrixHeight = 100;
// Size of each cell in pixels:
const int CellWidth = 20;
const int CellHeight = 20;
But that would be too slow to do each frame (for a 100x100 matrix).
You could update an off-screen image like this if you were updating only individual cells whenever they changed.
You could then place a PictureBox control on the form, and set its Image property to the Bitmap updated here. You can create that bitmap (parameter to this method) with new Bitmap(MatrixWidth * CellWidth, MatrixHeight * CellHeight) .
Alternatively, you could draw to the form's canvass in the OnPaint event handler (the Draw method in your question).
Assuming that the window scrolls (and not all of the matrix is visible within the viewport at any time),
you would loop through only the visible cells and make them draw themselves to the form's Graphics context.
The form's Graphics context is the property Graphics on the event arguments object. This is what you need for the Box.Draw method above. When drawing each cell, you have to take account of its position in the grid and how the window is scrolled, in order to calculate the coordinates to be passed to that method.
On each invocation of the OnPaint event, only part of the window has to be redrawn. This part is given by the property ClipRectangle . (Windows breaks down the area to be redrawn into one or more rectangles, and does a call for each rectangle.)
(ClipRectangle could be the whole window.)
Drawing outside of this is allowed but can be inefficient.
So, for efficiency, you should loop through only all cells that fall at least partly within ClipRectangle, calculate their position within the visible area and call their Box.Draw.
The coordinates used with the Graphics class are the visible coordinates, so (0,0) is the top left of the window, regardless of scrolling.
See these properties for the scroll position:
Form.HorizontalScroll.Value
Form.VerticalScroll.Value

Related

How to draw simple shapes onto a WPF xaml-defined canvas programatically

I am porting (or attempting to) legacy code from a very slow WinForms app to WPF. The WinForms app falls down at the rendering of large images, and it has to rerender very frequently as the user pans around so it has to go.
I have a system in place to draw a canvas in xaml that allows zooming and panning with a custom child of Border, in the xaml it is named imaginatively as "canvas". I am having issues drawing simple ellipses to the canvas from other classes.
namespace ZoomPan
{
public class DisplayManager
{
protected void DrawIcon(Locale locale, Color color)
{
Brush sensorBrush = new SolidColorBrush(color);
Brush sensorOutline = new SolidColorBrush(Colors.Black);
int x = locale.x;
int y = locale.y;
halfSize = 10;
DrawEllipse(locale.Name, sensorBrush, sensorOutline, x - halfSize, y - halfSize, 2 * halfSize, 2 * halfSize);
}
public void DrawEllipse(string name, Brush sensorBrush, Brush sensorPen, int x, int y, int width, int height)
{
double thickness = 5;
Ellipse ellipse = new Ellipse();
/*
Define ellipse
*/
canvas.Children.Add(ellipse);
}
}
}
This was my first attempt, which throws up errors around the final canvas.Children line, namely "The name 'canvas' does not exist in the current context"
I tried moving DrawEllipse to a separate class here, which throws up different errors
namespace ZoomPan
{
public class DisplayManager
{
protected void DrawIcon(Locale locale, Color color)
{
Brush sensorBrush = new SolidColorBrush(color);
Brush sensorOutline = new SolidColorBrush(Colors.Black);
int x = locale.x;
int y = locale.y;
halfSize = 10;
DrawEllipse(locale.Name, sensorBrush, sensorOutline, x - halfSize, y - halfSize, 2 * halfSize, 2 * halfSize);
}
}
public class MainWindow : Window
{
public void DrawEllipse(string name, Brush sensorBrush, Brush sensorPen, int x, int y, int width, int height)
{
double thickness = 5;
Ellipse ellipse = new Ellipse();
/*
Define ellipse
*/
canvas.Children.Add(ellipse);
}
}
}
These errors are around the call to DrawEllipse in the first class:
"The name 'DrawEllipse' does not exist in the current context"
and when I add MainWindow. before the DrawEllipse:
"An object reference is required for the non-static field, method, or property 'ZoomPan.MainWindow.DrawEllipse(string, System.Windows.Media.Brush, System.Windows.Media.Brush, int, int, int, int)'"
When I first played around with WPF, I could get it to display onto the canvas when the DrawEllipse method was inside the public class MainWindow:Window of the MainWindow.xaml.cs file, hence the second attempt above.
I feel I am missing something obvious, and couldn't find any exact dupes of this question on the site.
The other classes need to be passed the canvas object to be able to use it.
Either pass the canvas to the DisplayManager through the constructor once so it can be stored and then used in all methods:
public class DisplayManager
{
Canvas canvas;
public DisplayManager(Canvas canvas)
{
this.canvas = canvas;
}
}
Or pass it into each method each time as the first parameter:
public class DisplayManager
{
public void DrawEllipse(Canvas canvas, string name, Brush sensorBrush, Brush sensorPen, int x, int y, int width, int height)
{
}
}

How can I clear the Graphics before drawing another letter?

private void timer1_Tick(object sender, EventArgs e)
{
counter--;
DrawLetter();
if (counter == 0)
{
t.Stop();
TakeScreenShot();
}
}
private void DrawLetter()
{
var letter = counter.ToString();
Graphics g = Graphics.FromHdc(GetDC(IntPtr.Zero));
float width = ((float)this.ClientRectangle.Width);
float height = ((float)this.ClientRectangle.Width);
float emSize = height;
Font font = new Font(FontFamily.GenericSansSerif, emSize, FontStyle.Regular);
font = FindBestFitFont(g, letter.ToString(), font, this.ClientRectangle.Size);
SizeF size = g.MeasureString(letter.ToString(), font);
g.DrawString(letter, font, new SolidBrush(Color.White), (width - size.Width) / 2, 0);
}
private Font FindBestFitFont(Graphics g, String text, Font font, Size proposedSize)
{
// Compute actual size, shrink if needed
while (true)
{
SizeF size = g.MeasureString(text, font);
// It fits, back out
if (size.Height <= proposedSize.Height &&
size.Width <= proposedSize.Width) { return font; }
// Try a smaller font (90% of old size)
Font oldFont = font;
font = new Font(font.Name, (float)(font.Size * .9), font.Style);
oldFont.Dispose();
}
}
void TakeScreenShot()
{
bmpScreenshot = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb);
gfxScreenshot = Graphics.FromImage(bmpScreenshot);
gfxScreenshot.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
bmpScreenshot.Save(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + #"\ScreenCaptures\newfile.png", ImageFormat.Png);
}
I am able to draw the string but it is writing on top of itself.
How can I clear it? Basically I want the countdown to appear on the screen then take a screenshot.
Right now the number is overwritten by another.
You can do the following: create an additional transparent form, and it will display timer values. This will allow you to erase the previous value. In addition, this will allow to get rid of the function call GetDC via PInvoke.
Form timerForm; // main form field
// Create and show additional transparent form before starting the timer
timerForm = new Form
{
FormBorderStyle = FormBorderStyle.None,
WindowState = FormWindowState.Maximized,
TransparencyKey = SystemColors.Control,
ShowInTaskbar = false
};
timerForm.Show();
timer.Start();
Change the method DrawLetter as follows
private void DrawLetter()
{
var letter = counter.ToString();
Graphics g = timerForm.CreateGraphics();
float width = ClientRectangle.Width;
float height = ClientRectangle.Width;
float emSize = height;
using (Font font1 = new Font(FontFamily.GenericSansSerif, emSize, FontStyle.Regular))
using (Font font2 = FindBestFitFont(g, letter, font1, ClientRectangle.Size))
using (var brush = new SolidBrush(Color.White))
{
SizeF size = g.MeasureString(letter, font2);
g.Clear(SystemColors.Control);
g.DrawString(letter, font2, brush, (width - size.Width) / 2, 0);
}
}
We must release all used resources like fonts and brushes. For this I applied using.
Change the timer tick event handler as follows
private void timer1_Tick(object sender, EventArgs e)
{
counter--;
DrawLetter();
if (counter == 0)
{
timer.Stop();
TakeScreenShot();
timerForm.Dispose(); // must release
}
}
FindBestFitFont and TakeScreenShot methods remain unchanged.
Draw your font to a different bitmap. Transparent background (or whatever doesn't invert, see below - perhaps black).
(now you could also draw it with a different colored shadow to mitigate drawing on similar colored background - but the natures of SRCINVERT/XOR, below, will mitigate this as well)
Use BitBlt to copy it to the screen
Use the SRCINVERT raster op.
(note: the colors may be different as it is XORing it with pixels underneath)
Now when is is time to erase, just make the same bitblt with the same contents as previous, the double XOR effect caused by SRCINVERT will have the effect of erasing it.
Then draw the next font.
Note: if desktop is updated between calls, all bets are off.
better...
Rather than attempting a transparent background, draw it on a white background. This will eliminate contrast issues with the font, eliminate concern with dynamic updates, and eliminate problems with erasing. Sometimes you have to admit - the method & code isn't the problem, the requirements are the problem. This all depends of course on the source of the requirements, etc.
If it needs to look professional, don't put the content on the screen, draw it after you take the screen capture.
If you end up using the transparent window approach, the screen shot may miss the transparent window. To get it, see this question:
Capture screenshot Including Semitransparent windows in .NET. (could be fixed by newer .net / newer windows versions)
You need to invalidate all the windows on the desktop by using the InvalidateRect function to erase the previously drawn letter.
See additional codes below for the DrawLetter method.
[DllImport("user32")]
private static extern bool InvalidateRect(IntPtr hwnd, IntPtr rect, bool bErase);
private void DrawLetter()
{
var letter = counter.ToString();
Graphics g = Graphics.FromHdc(GetDC(IntPtr.Zero));
float width = ((float)this.ClientRectangle.Width);
float height = ((float)this.ClientRectangle.Width);
float emSize = height;
Font font = new Font(FontFamily.GenericSansSerif, emSize, FontStyle.Regular);
font = FindBestFitFont(g, letter.ToString(), font, this.ClientRectangle.Size);
SizeF size = g.MeasureString(letter.ToString(), font);
// Invalidate all the windows.
InvalidateRect(IntPtr.Zero, IntPtr.Zero, true);
// Sometimes, the letter is drawn before the windows are invalidated.
// To fix that, add a small delay before drawing the letter.
System.Threading.Thread.Sleep(100);
// Finally, draw the letter.
g.DrawString(letter, font, new SolidBrush(Color.White), (width - size.Width) / 2, 0);
}
A solution is:
You must take a snapshot of that area you want to show counter before all things. Then call DrawImage function to draw snapshot image before call DrawString function every time.

c# how to trigger click only at certain position

I have a C# UserControl. Within the it I have overriden the OnPaint method. Then I draw a circle inside it.
Bitmap GraphicsImage = new Bitmap(24, 24, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
Graphics.FromImage(GraphicsImage).Clear(btnColor);
Graphics graphics = e.Graphics;
SolidBrush myBrush = new SolidBrush(btnColor);
Pen myPen = new Pen(btnColor);
// Draw the button in the form of a circle
graphics.DrawEllipse(myPen, 0, 0, 40, 40);
graphics.FillEllipse(myBrush, new Rectangle(0, 0, 40, 40));
Here is the Image
What I want is to trigger the mouse click event only when the mouse is inside the circle, because the usercontrol is bigger.
You can trigger all click events on the user control, and check if the mouse position is inside the circle
private void yourcontrol_Click(object sender, EventArgs e)
{
Point pt = yourcontrol.PointToClient(System.Windows.Forms.Control.MousePosition);
//check if point is in the circle with
if (Math.Sqrt(Math.Pow(pt.X - xCenterOfCircle, 2) + Math.Pow(pt.Y - yCenterOfCOircle, 2)) < radius)
{
//do something
}
}
xCenterOfCircle has to be the x position of the center of your circle and yCenterOfCircle the y position. I assume in your example it is the center of your control and the radius would be half the size of your control.
I would do that by overriding OnMouseDown:
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Control.MouseClick"/> event.
/// </summary>
/// <param name="e">An <see cref="T:System.Windows.Forms.MouseEventArgs"/> that contains the event data. </param>
protected override void OnMouseClick(MouseEventArgs e)
{
var centerX = Left + Width/2.0;
var centerY = Top + Height/2.0;
var dist = (e.X - centerX)*(e.X - centerX) + (e.Y - centerY)*(e.Y - centerY);
if(dist <= (radius*radius))
{
base.OnMouseClick(e);
}
}
So, if the mouse is within a given radius of the center of the control (using Pythagoras's theorem, pass on the click event. Otherwise, ignore it, as the control hasn't actually been clicked.
You can then add event handlers to control.Click as normal.
Edit: to duplicate the example code exactly, change:
var centerX = 20; //(40-0) / 2
var centerY = 20;

Issues with Rendering a Bitmap

I am currently working on a histogram renderer that renders bitmaps onto the Grasshopper canvas. There are a total of two bitmaps, both of them explained below
private readonly Bitmap _image;
and:
private readonly Bitmap _overlayedImage;
The Bitmap instance with the name _image looks like this:
_bitmap http://puu.sh/6mUk4/20b879710a.png
While the Bitmap instance with the name _overlayedImage looks like this:
Basically, _overlayedImage is a bitmap that is created using the _image bitmap, and as the name suggests, overlays the text (that you can see in the image I posted) and adds a black background to it. This is how it is assigned
_overlayedImage = overlayBitmap(_image, width * 3, height * 3, times, dates, colors);
(The * 3 is used to resize the image).
An issue I currently have is multi-fold.
Using this method, I am able to render _image onto the canvas.
The code is like this:
protected override void Render(Grasshopper.GUI.Canvas.GH_Canvas canvas, Graphics graphics, Grasshopper.GUI.Canvas.GH_CanvasChannel channel) {
// Render the default component.
base.Render(canvas, graphics, channel);
// Now render our bitmap if it exists.
if (channel == Grasshopper.GUI.Canvas.GH_CanvasChannel.Wires) {
var comp = Owner as KT_HeatmapComponent;
if (comp == null)
return;
List<HeatMap> maps = comp.CachedHeatmaps;
if (maps == null)
return;
if (maps.Count == 0)
return;
int x = Convert.ToInt32(Bounds.X + Bounds.Width / 2);
int y = Convert.ToInt32(Bounds.Bottom + 10);
for (int i = 0; i < maps.Count; i++) {
Bitmap image = maps[i].overlayedImage;
if (image == null)
continue;
Rectangle mapBounds = new Rectangle(x, y, maps[i].Width, maps[i].Height);
mapBounds.X -= mapBounds.Width / 2;
Rectangle edgeBounds = mapBounds;
GH_Capsule capsule = GH_Capsule.CreateCapsule(edgeBounds, GH_Palette.Normal);
capsule.Render(graphics, Selected, false, false);
capsule.Dispose();
graphics.DrawImage(image, mapBounds);
graphics.DrawRectangle(Pens.Black, mapBounds);
// some graphics interpolation and bicubic methods
y = edgeBounds.Bottom - (mapBounds.Height) - 4;
}
}
}
As per what comp.CachedHeatmaps; is:
private readonly List<HeatMap> _maps = new List<HeatMap>();
internal List<HeatMap> CachedHeatmaps {
get { return _maps; }
}
However, whenever I try to use Render() on the _overlayedImage, I am unable to do so.
I have isolated the issue to the Render() method, and it seems this line
Rectangle mapBounds = new Rectangle(x, y, maps[i].Width, maps[i].Height); is the main issue, as maps[i].Width and maps[i].Height returns 1 and 100 respectively, which are coincidentally the dimensions of the legend, which are 100 pixels vertically and 1 pixel horizontally.
I apologize for the decently long question, but I don't think I could have explained it any other way.
It turns out there are two issues:
In my main method I used _overlayedImage.Dispose(), which effectively destroyed the image before it was even displayed onto the canvas.
Also, my issue isolation was also correct. This line resulted in the thing rendering correctly:
Rectangle mapBounds = new Rectangle(x, y, maps[i].overlayedImage.Width, maps[i].overlayedImage.Height);
Resulting component:

Extract and display parameter in a dialog?

I am working on a small project and packing it into GUI. The reference source code is DrawTools(Download source code - 61.1 Kb).
The reference source code demos a drawing tool in C# WinForms.
The function is to draw different figures like rectangle, ellipse, polygon, etc.
I want to use the location and size information of these figures to do further work, so if I draw a rectangle in the draw area, could C# WinForms returns the parameter of this figure(eg. x,y,width,height in the DrawRectangle.cs)?
Code as follow:
public DrawRectangle(int x, int y, int width, int height)
{
rectangle.X = x;
rectangle.Y = y;
rectangle.Width = width;
rectangle.Height = height;
Initialize();
}
Further more, How to get the returned parameters and then displayed in a new dialog?
when you draw these shapes: rectangle, ellipse, polygon, etc. you are using the location and width and height of them. if you want to save them to an object create one and save them in a list of some other structure...
for example:
List<object> shapes = new List<object>();
private void drawSquare(int x1, int y1, int x2, int y2)
{
shapes.Add(new Rectangle(x1, y1, x2, y2));
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics;
foreach (var shape in shapes)
{
if (shape is Rectangle)
{
g.DrawRectangle(new Pen(Color.Black), (Rectangle)shape);
}
}
}
this is just a small example, you should check the OnPaint method and Graphics to get more information on what you can and should do
You can add some event to support notifying what is happening, something like this:
public class InitRectangleEventArgs : EventArgs {
public Rectangle Rectangle {get;set;}
}
public delegate void InitRectangleEventHandler(object sender, InitRectangleEventArgs e);
public event InitRectangleEventHandler InitRectangle;
public DrawRectangle(int x, int y, int width, int height)
{
rectangle.X = x;
rectangle.Y = y;
rectangle.Width = width;
rectangle.Height = height;
if(InitRectangle != null) InitRectangle(this, new InitRectangleEventArgs { Rectangle = new Rectangle(x,y,width,height)});
Initialize();
}
//To use it, just subscribe the event so that you can know the
//info of the Rectangle everytime it is initialized
InitRectangle += (s,e) => {
//Get the info from the Rectangle property of e: e.Rectangle
//....
};

Categories