I'm trying to redraw a shape displayed on a SKCanvas the method (ChangeShape) works fine the 1st time it's called from the PaintSurface handler but when I attempt to call it again the app crashes and I receive the error "Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR)".
I have tried to call the handler directly and Initialise a completely new SKSurface, but it still results in the same error.
public LinearLayout ImageDivisionLayout_Obj;
public SKCanvasView ImageViewActivity_Obj;
public SKSurface ImageCanvas;
public SKCanvas canvasSK;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.activity_image);
ImageViewActivity_Obj = new SKCanvasView(this);
ImageViewActivity_Obj.PaintSurface += ImageViewActivity_Obj_PaintSurface;
ImageDivisionLayout_Obj = (LinearLayout)FindViewById(Resource.Id.ImageLayoutDivision);
ImageDivisionLayout_Obj.AddView(ImageViewActivity_Obj);
private void ImageViewActivity_Obj_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
SKImageInfo info = e.Info;
SKSurface surface = e.Surface;
ImageCanvas = surface;
SetupCanvas();
}
public void SetupCanvas()
{
ShapePaint = new SKPaint();
ShapePaint.Color = SKColor.Parse("#F00000");
ShapePaint.StrokeWidth = 25;
ShapePaint.Style = SKPaintStyle.Stroke;
ImageCanvas.Canvas.DrawBitmap(
AndroidExtensions.ToSKBitmap(ImageTaken),
new SKRect(0, 0, ImageViewActivity_Obj.Width,
ImageViewActivity_Obj.Height));
ChangeShape();
ImageCanvas.Canvas.Save();
}
public void ChangeShape()
{
float X1 = CurrentShape.X;
float Y1 = CurrentShape.Y;
float X2 = CurrentShape.X + CurrentShape.width;
float Y2 = CurrentShape.Y + CurrentShape.height;
if (CurrentShape.name == "Rectangle")
{
ImageCanvas.Canvas.DrawRect(X1, Y1, X2, Y2, ShapePaint);
}
else if (CurrentShape.name == "Circle")
{
ImageCanvas.Canvas.DrawCircle(new SKPoint(X2 - X1, Y2 - Y1), CurrentShape.width / 2, ShapePaint);
}
}
Having a look at what you are doing, I can see that this probably will not work on most platforms. The SKSurface that you are storing in the ImageCanvas field will be destroyed as soon as you leave the ImageViewActivity_Obj_PaintSurface method. The surface and canvas objects are transient and only live as long as the method is running.
I am assuming you are trying to create some sort of drawing app? If so, there are two approaches to this (although the smarter people may suggest more). You can either store all the operations in a list of some sort. Basically, if I draw a rectangle, store the operation and the bounds.
Or, you can create a background surface with SKSurface.Create(...) and then draw on that. When drawing to the screen, just copy the image from the background canvas to the screen canvas. Then, you can also just save the background canvas without trying to capture the screen canvas to save to disk.
I did a simple finger paint app here that you can check out for ideas: https://github.com/mattleibow/SkiaSharpDemo/blob/master/SkiaSharpDemo/SkiaSharpDemo/MainPage.xaml.cs
Related
I'm trying to access the variable "g" from a different method, it does not throw an error but doesn't write to the screen. Any way to use the graphics class from different methods? Paint Method:
public static Graphics g;
private void paintClass(object sender, PaintEventArgs e)
{
e.Graphics = g;
}
Method Im Trying To Access "g" From:
public void drawline(){
g.DrawLine(new Pen(Color.AliceBlue), 10, 10, 20, 20);
}
Note These Code Snippets Are from The Same Class
If this isn't possible, I am just trying to figure out a way to paint to the screen from a method that isn't the paint method.
Instead of storing it, pass it directly from your event:
private void paintClass(object sender, PaintEventArgs e)
{
drawline(e.Graphics);
}
public void drawline(Graphics g){
g.DrawLine(new Pen(Color.AliceBlue), 10, 10, 20, 20);
}
This approach would make more sense if the "draw" method was part of a different class. For example, if you had a collection of custom class instances and you pass the graphics to each one so they can each draw themselves onto a surface (with the graphics provided from the paint event of that surface).
I am guessing you could obtain what you want by using CreateGraphics().
var g = myButton.CreateGraphics();
g.DrawLine(Pens.Red, 0, 0, 20, 20);
Replace myButton with the Control or Form you want to draw on.
This may turn out to not be the right approach though. The drawing will disappear e.g. when the form is minimized, maximized, gets covered by another form etc.
The recommended pattern is to use the Paint event - see https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.paint?view=windowsdesktop-6
One thing you could try is that every time you call something like the drawline() method that you mention in your post, add this action to a List. Then, when the List changes, have your paint class fire a Refresh event that the containing control can use to ensure the canvas will be invalidated. When the control calls Refresh() on itself, the OnPaint override is called and the items in the list are iterated and drawn (or in some cases redrawn) in a legitimate Graphics context that doesn't rely on the temporary fix of calling CreateGraphics ad hoc. You could then make these calls from your main form:
Diag Button - Diagonal line adding 25 to offset each time.
private void buttonDiag_Click(object sender, EventArgs e)
{
_paintClass.DrawDiagonal(GetNextTestColor(), offsetX: 25 * _testCountDiag++);
}
Clicking the button 3x executes the Drawline with a different random known color each time.
Line Button - Draw a line between two points.
private void buttonLine_Click(object sender, EventArgs e)
{
var offsetY = 15 * _testCountLine++;
_paintClass.Drawline(
GetNextTestColor(),
new PointF(0, 100 + offsetY),
new PointF(ClientRectangle.Width, 100 + offsetY));
}
Under the hood
The PaintClass inherits List<PaintClassContext> where:
class PaintClassContext
{
public PaintOp PaintOp { get; set; }
public Color Color { get; set; }
public PointF Start { get; set; }
public PointF End { get; set; }
public float OffsetX { get; set; }
public float OffsetY { get; set; }
}
enum PaintOp{ DrawLine, Clear, DrawDiagonal}
Example: Implementation of DrawDiagonal
Add a new context to the current list.
public void DrawDiagonal(Color color, float offsetX = 0, float offsetY = 0) =>
Add(new PaintClassContext
{
PaintOp = PaintOp.DrawDiagonal,
Color = color,
OffsetX = offsetX,
OffsetY = offsetY,
});
Every time a new action is added, the Refresh event is fired and this repaints the canvas.
public new void Add(PaintClassContext context)
{
base.Add(context);
_modified = true;
Refresh?.Invoke(this, EventArgs.Empty);
}
The MainForm will be subscribed to the PaintClass.Refresh event and calls Refresh().
_paintClass.Refresh += (sender, e) =>
{
// Causes the control to repaint.
Refresh();
Text = CurrentTestColor.ToString();
};
This causes the control to redraw, where the PaintAll will iterate its current items and paint them in a legitimate graphics context rather than trying to "finesse" one.
PaintClass _paintClass = new PaintClass();
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
_paintClass.PaintAll(e.Graphics, always: false);
}
In my application, there are 2 windows and both contain a PictureBox. The first (pb1) allows interaction and the image can be changed through click- and mouseMove-events. These events call pb1.Invalidate(); which works fine.
I want the second PictureBox (pb2) to redraw as well so I call pb2.Invalidate() from the paint-event of pb1. [Just for context, the second PictureBox shows nearly the same Image but on a bigger scale and some parts of the drawing will be left out in the future so I use the same Method in both paint events which decides what to draw and what not]
It works but it's "laggy" and I want it to be as smooth as the paint on the first PictureBox. I reduced the paint event just to a grid for test purposes.
Both windows are double buffered.
I tried replacing the picture boxes with SKGLControls from SkiaSharp (which should have better performance). The example code still uses the SkiaEvents so don't be confused if the problem occurs with both controls.
I tried to use .Update() or .Refresh() instead of .Invalidate() but i guess its to much to handle, the application just crashes..
Here is the method that is called by both OnPaint events
public void Update(SKPaintGLSurfaceEventArgs e, bool bigscreen)
{
SKCanvas canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Beige);
//Zoom to specified area
SKMatrix matrix = SKMatrix.Identity;
if (!bigscreen)
{
matrix = matrix.PostConcat(SKMatrix.CreateScale(canvasSize / (float)zoomArea.Width, canvasSize / (float)zoomArea.Height));
}
else
{
matrix = matrix.PostConcat(SKMatrix.CreateScale(bigCanvasSize / (float)zoomArea.Width, bigCanvasSize / (float)zoomArea.Height));
}
matrix = matrix.PreConcat(SKMatrix.CreateTranslation(-zoomArea.X, -zoomArea.Y));
canvas.SetMatrix(matrix);
DrawGrid(canvas);
}
and the grid-draw method
private void DrawGrid(SKCanvas canvas)
{
using (SKPaint paint = new SKPaint() { IsAntialias = true,Color=SKColors.LightGray,StrokeWidth = 1})
{
canvas.DrawLine(0, 0, 0, gridCanvas.Height, paint); //Size gridCanvas is always the same at the moment and defines the space where the grid is drawn
canvas.DrawLine(0, 0, gridCanvas.Width, 0, paint);
for (int i = 0; i <= (gridCanvas.Width - gridoffsetX) / pxPerSquare; i++)
{
canvas.DrawLine(i * pxPerSquare + gridoffsetX, 0, i * pxPerSquare + gridoffsetX, gridCanvas.Height, paint);
}
for (int i = 0; i <= (gridCanvas.Height - gridoffsetY) / pxPerSquare; i++)
{
canvas.DrawLine(0, i * pxPerSquare + gridoffsetY, gridCanvas.Width, i * pxPerSquare + gridoffsetY, paint);
}
}
}
and finally the original Paint Event
private void Pb1_PaintSurface(object sender, SKPaintGLSurfaceEventArgs e)
{
win2.UpdateDrawing(); //Just calls .Invalidate() on pb2
painter.Update(e, false);
}
examplePicture
So my question is: Is there a way to make both controls draw at nearly the same time without delay, although I don't understand why the first PictureBox draws in real time and the second doesn't...
Thanks!
after searching for day i found this page right after posting, which helped me:
Onpaint events (invalidated) changing execution order after a period normal operation (runtime)
This question is asked before but since it doesn't work and my lack of reputation point(I tried to comment at question but I couldn't) I had to ask this question again.
This is the link of the quustion asked before;
How to zoom at a point in picturebox
I used the code which is shown in the link but when I run it the point or shape disappear.
here is my code;
public partial class Form1 : Form
{
private Matrix transform = new Matrix();
private double m_dZoomscale = 1.0;
public static double s_dScrollValue = .1;
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Transform = transform;
Pen mypen = new Pen(Color.Red,5);
Rectangle rect = new Rectangle(10, 10, 30, 30);
e.Graphics.DrawRectangle(mypen, rect);
}
protected override void OnMouseWheel(MouseEventArgs mea)
{
pictureBox1.Focus();
if (pictureBox1.Focused == true && mea.Delta != 0)
{
ZoomScroll(mea.Location, mea.Delta > 0);
}
}
private void ZoomScroll(Point location, bool zoomIn)
{
transform.Translate(-location.X, -location.Y);
if (zoomIn)
transform.Scale((float)s_dScrollValue, (float)s_dScrollValue);
else
transform.Scale((float)-s_dScrollValue, (float)-s_dScrollValue);
transform.Translate(location.X, location.Y);
pictureBox1.Invalidate();
}
The answer you are referencing cannot possibly work. I have no idea why it was accepted, nor up-voted. Except that at some time in the past, I apparently up-voted it as well. I don't know what I was thinking.
Anyway, that code has some problems:
It uses the mouse coordinates passed in directly, rather than converting them to the coordinate system for the PictureBox control. The coordinates passed to the OnMouseWheel() method are relative to the Form itself, so only if the PictureBox top-left coincides with the Form's upper-left corner would that work.
More problematically, the code is completely misusing the Matrix.Scale() method, passing a value that seems intended to be a delta for the scale, when in fact the Scale() method accepts a factor for the scale. This has two implications:
Passing a negative value is wrong, because negative values flip the coordinate system, rather than reducing the scale, and
Passing an increment value is wrong, because the value passed will be multiplied with the current scaling to get the new scaling.
Also problematic is that the code applies the matrix transformations in the wrong order, because the default order is "prepend", not "append" (I find the latter more natural to work with, but I assume there's some reason known to those who specialize in matrix math that explains why the default is the former).
There is also the relatively minor issue that, even ignoring the above, allowing the user to adjust the scale factor arbitrarily will eventually lead to an out-of-range value. It would be better for the code to limit the scale to something reasonable.
Here is a version of your code, modified so that it addresses all of these issues:
private Matrix transform = new Matrix();
private float m_dZoomscale = 1.0f;
public const float s_dScrollValue = 0.1f;
public Form1()
{
InitializeComponent();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Transform = transform;
Pen mypen = new Pen(Color.Red, 5);
Rectangle rect = new Rectangle(10, 10, 30, 30);
e.Graphics.DrawRectangle(mypen, rect);
}
protected override void OnMouseWheel(MouseEventArgs mea)
{
pictureBox1.Focus();
if (pictureBox1.Focused == true && mea.Delta != 0)
{
// Map the Form-centric mouse location to the PictureBox client coordinate system
Point pictureBoxPoint = pictureBox1.PointToClient(this.PointToScreen(mea.Location));
ZoomScroll(pictureBoxPoint, mea.Delta > 0);
}
}
private void ZoomScroll(Point location, bool zoomIn)
{
// Figure out what the new scale will be. Ensure the scale factor remains between
// 1% and 1000%
float newScale = Math.Min(Math.Max(m_dZoomscale + (zoomIn ? s_dScrollValue : -s_dScrollValue), 0.1f), 10);
if (newScale != m_dZoomscale)
{
float adjust = newScale / m_dZoomscale;
m_dZoomscale = newScale;
// Translate mouse point to origin
transform.Translate(-location.X, -location.Y, MatrixOrder.Append);
// Scale view
transform.Scale(adjust, adjust, MatrixOrder.Append);
// Translate origin back to original mouse point.
transform.Translate(location.X, location.Y, MatrixOrder.Append);
pictureBox1.Invalidate();
}
}
With this code, you will find that no matter where you place the mouse before adjusting the mouse wheel, the rendered image will scale while keeping the point under the mouse fixed in place.
Note:
I took a look at some of the similar questions on Stack Overflow, and there are a few that might also be useful to you. Some of the answers overcomplicate things, in my opinion, but all should work. See:
Zoom To Point Not Working As Expected
Zoom in on a fixed point using matrices
Zooming graphics without scrolling
I've seen few questions about this problem, I tried every solution but none of them worked for my case.
My code is working; this image shows what happens when I click on Draw button.
I need to zoom on that drawing.Is it possible to code something like autocad feature "zoom/extent"?
Pen myPen = new Pen(Color.Black);
int centerpointx, centerpointy;
private void pictureBoxDraw_Paint(object sender, PaintEventArgs e)
{
centerpointx = pictureBoxDraw.Size.Width/2;
centerpointy = pictureBoxDraw.Size.Height/2;
myPen.Width = 2;
if (binary > 0)
{
var sizecrestgeo = 40;
var distancearraycrestgeo = new float[sizecrestgeo];
var elevationarraycrestgeo = new float[sizecrestgeo];
for (int i = 0; i < sizecrestgeo; i++)
{
distancearraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[0].Value.ToString());
elevationarraycrestgeo[i] = float.Parse(dataGridViewCrestGeo.Rows[i].Cells[1].Value.ToString())*-1;
}
for (int i=0; i < sizecrestgeo-1; i++)
{
e.Graphics.DrawLine(myPen, distancearraycrestgeo[i]+centerpointx, elevationarraycrestgeo[i]+centerpointy, distancearraycrestgeo[i + 1]+centerpointx, elevationarraycrestgeo[i + 1]+centerpointy);
}
}
else
{
}
}
private void buttonDraw_Click_1(object sender, EventArgs e)
{
if (Hd > 0.0001)
{
binary = 1;
pictureBoxDraw.Invalidate();
}
else
{
MessageBox.Show("No data to draw, perform analysis first.");
}
}
private void buttoncleardraw_Click(object sender, EventArgs e)
{
binary = 0;
pictureBoxDraw.Invalidate();
}
}
This is not so hard, provided you know all the puzzle pieces.
Let's start with the obvious one:
You can scale the Graphics object to create zoomed graphics with ScaleTransform.
As I mentioned, this will include the widths of pens, font sizes and also any images you draw (though not the hatches of a HatchBrush).
You also asked about keeping the drawing 'centered'. This is a non-obvious concept: Just what is the center of your drawing surface??
When zooming (just like rotating) you always need to know the center point of the zoom (or the rotation.) By default this is the origin (0,0). I chose the center of the Panel. You may want to pick some other point..
Once you do you can move the origin of the graphics viewport to this point with TranslateTransform.
Once you have achieved all this you almost certainly will want to allow scrolling.
To do so you have two options:
You can keep AutoScroll = false and nest the canvas control inside another control, usually a Panel, which has AutoScroll = true; next make the canvas control big enough to always hold your drawing and you're done.
Or you can turn on AutoScroll for the canvas control and also set a large enough AutoScrollMinSize. If you then add the current scrolling position to the translation you are also done. Let's see this solution in action:
This is the code in the Paint event:
Size sz = panel3.ClientSize;
Point center = new Point(sz.Width / 2, sz.Height / 2);
Graphics g = e.Graphics;
// center point for testing only!
g.DrawEllipse(Pens.Orange, center.X - 3, center.Y - 3, 6, 6);
// you determine the value of the zooming!
float zoom = (trackBar1.Value+1) / 3f;
// move the scrolled center to the origon
g.TranslateTransform(center.X + panel3.AutoScrollPosition.X,
center.Y + panel3.AutoScrollPosition.Y);
// scale the graphics
g.ScaleTransform(zoom, zoom);
// draw some stuff..
using(Pen pen = new Pen(Color.Yellow, 0.1f))
for (int i = -100; i < 100; i+= 10)
g.DrawEllipse(Pens.Yellow, i-22,i-22,44,44);
A few notes:
I draw an orange circle in the center to show this point is invariant.
My coordinates go from the negative to the positive so you can see that this works nicely.
I draw with a tiny pen width; so the width of the drawing only changes once the resulting pen goes over 1 pixel. Anything draw will always be draw with 1 pxiel width, though.
I first translate and then scale so I don't have to calculate scaled poitions.
The only line in the TrackBar's Scroll event is to trigger the Paint event: panel3.Invalidate();
The only settings needed for the Panel are
panel3.AutoScroll = true;
panel3.AutoScrollMinSize = new Size(500, 500); // use the size you want to allow!
However to avoid flicker it is highly recommended to use a DoubleBuffered control, maybe a Panel subclass like this:
class DrawPanel : Panel
{
public DrawPanel() { DoubleBuffered = true; }
}
Update: Instead of a Panel, which is a Container control and not really meant to draw onto you can use a Picturebox or a Label (with Autosize=false); both have the DoubleBuffered property turned on out of the box and support drawing better than Panels do.
Graphics.ScaleTransform() is how you can zoom. Try using something like this inside your paint event handler:
e.Graphics.ScaleTransform(2.0F, 2.0F);
Today I am trying to solve problem with a blinking panel, when I draw onto it.
Lots of threads I read, like these:
how to stop flickering C# winforms,
Double buffering with Panel,
How can I draw on Panel so it does not blink?
So I tried to draw onto PictureBox, MyPanel with doubleBuffered, but the best solution I found, when I read, that I can't use g.Clear() every time, after that, even on non-doubleBuffered panel, blinking disappeared.
I even read, that I should free Graphics after draw is done. So I use everywhere using(Graphics g = panel.CreateGraphics()).
So my question, is it a great idea to create graphics for bitmap only when I draw something to it? Because before I created Bitmap, and Graphics (only for this bitmap, not for all components), so I had Graphics available for this bitmap every time
Here is my code:
public void newSizeDrawing()
{
Size size = collector.getLetterSize(selectedName);
Size drawingSize = new Size(size.Width * (pixelSizeArray[pixelSize] + 1),size.Height * (pixelSizeArray[pixelSize] + 1));
bitmapDraw = new Bitmap(drawingSize.Width, drawingSize.Height);
int width = (this.MinimumSize.Width - panelDraw.MinimumSize.Width) + drawingSize.Width + 10;
int height = (this.MinimumSize.Height - panelDraw.MinimumSize.Height) + drawingSize.Height + 10;
this.Size = new Size(
(width > this.MinimumSize.Width) ? width : this.MinimumSize.Width,
(height > this.MinimumSize.Height) ? height : this.MinimumSize.Height);
zeroDrawPosition = new Point((panelDraw.Size.Width - bitmapDraw.Width) / 2 - 1, (panelDraw.Size.Height - bitmapDraw.Height) / 2 - 1);
using (Graphics g = panelDraw.CreateGraphics())
{
g.Clear(panelDraw.BackColor);
}
redrawDrawingLetter();
}
public void redrawDrawingLetter()
{
bool[][] grid = collector.getArray(selectedName);
using (Graphics graphicDraw = Graphics.FromImage(bitmapDraw))
{
graphicDraw.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighSpeed;
graphicDraw.Clear(panelDraw.BackColor);
int pxSize = pixelSizeArray[pixelSize];
for (int y = 0; y < grid.Length; y++)
{
for (int x = 0; x < grid[y].Length; x++)
{
graphicDraw.FillRectangle((grid[y][x] ? Brushes.Black : Brushes.White), x * (pxSize + 1), y * (pxSize + 1), pxSize, pxSize);
}
}
}
redrawDrawingPanel();
}
private void redrawDrawingPanel()
{
using (Graphics g = panelDraw.CreateGraphics())
{
if (bitmapDraw != null)
g.DrawImage(bitmapDraw, zeroDrawPosition);
}
}
private void panelDraw_Paint(object sender, PaintEventArgs e)
{
redrawDrawingPanel();
}
Nobody can explain to me how to draw in C# the best way. So maybe my code isn't good, but that is reason why I asking how to do it correctly.
newSizeDrawing is called by myself only, when user click on + or - button. I have bool double-dimension array if pixel is on or off. This is program for drawing letters for microchips and LED display (often 8px height of letter).
I wrote a method that checks if the mouse moved from one "pixel" to another, so I don't redraw it after every call mouseMove event, because "pixel" can be from 10x10 px to 30x30 px.
private void panelDraw_Paint(object sender, PaintEventArgs e)
{
redrawDrawingPanel();
}
This is fundamentally wrong. The Paint event passes e.Graphics to let you draw whatever you want to paint. When you turn on double-buffering, e.Graphics refers to a bitmap, it is initialized with the BackColor. You then proceed to drawing using another Graphics object you got from CreateGraphics(). That one draws directly to the screen.
The flicker effect you see if very pronounced. For a split second you see what the other Graphics context draws. Then your panelDraw_Paint() method returns and Winforms draws the double-buffered bitmap. There's nothing on it so it immediately erases what you drew.
Modify the redrawDrawingPanel() method and give it an argument of type Graphics. Pass e.Graphics in the call. And only use that Graphics object, remove all calls to CreateGraphics().