I have a framework for drawing made in C#. Recently I've been trying to do something and noticed the following problem:
When I draw a Geometry manually on DrawingVisual using its RenderOpen, and then move it using TranslateTransform, I sometime lose the fill.
To see what happens, you can insert the following framework element to a Window and run it:
class MyVisual : FrameworkElement {
private readonly DrawingVisual _visual = new DrawingVisual();
private readonly Geometry _geom;
private readonly Random _r = new Random();
public MyVisual()
{
AddVisualChild(_visual);
_geom = Geometry.Parse("M 95 100 L 130 130 95 160 Z").Clone();
_geom.Transform = new TranslateTransform();
UpdateVisual();
}
public void MoveGeom() {
var transform = _geom.Transform as TranslateTransform;
var x = _r.Next(-60, 200);
var y = _r.Next(-60, 200);
transform.X = x;
transform.Y = y;
}
void UpdateVisual()
{
using (var dc = _visual.RenderOpen())
{
UpdateVisual(dc);
}
}
private void UpdateVisual(DrawingContext dc)
{
var color = Brushes.Red;
var pen = new Pen(Brushes.Blue, 1);
dc.DrawGeometry(color, pen, _geom);
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => _visual;
}
public partial class MainWindow : Window
{
Timer _t = new Timer(500) { AutoReset = true };
public MainWindow()
{
InitializeComponent();
_t.Elapsed += (x, y) => Dispatcher.Invoke(() => _vis.MoveGeom());
_t.Start();
}
}
Is this a known issue, is there some simple workaround for it, or some other solution?
This solution seems to solve this issue:
public void MoveGeom()
{
var x = _r.Next(-60, 200);
var y = _r.Next(-60, 200);
_geom.Transform = new TranslateTransform(x, y);
}
Obviously there is a problem when the two coordinates are set separately because setting the single coordinates causes two updates and the rendering process is confused by that in any way.
Related
Sorry this is so long, I wanted to proved enough code as it's hard to explain on it's own
I'm setting up a small test program to make a 2D visual gauge. The structure of the project is that I have a class; 'DirectX11Renderer' which initialises all the SharpDX objects to set up the factories, devices, back buffers, rendertargets, swapchains and layers.
This class then contains drawing methods for drawing basic shapes, filled shapes and text, they can likely be ignored, however the idea of the program is to be able to render the static content of the gauge on one layer (the background, stuff that doesn't really move) and the dynamic content on another layer, so each drawing method has an enum as an argument which specifies the layer to draw on, and then that is passed into this method, which starts drawing on the render target (if not already), pops the last pushed layer (if there was one) and pushes the layer that is required to be drawn on:
private void StartDrawingAndSwitchLayer(TargetLayer layer, bool draw = true)
{
if (draw && !isDrawing)
{
d2dRenderTarget.BeginDraw();
isDrawing = true;
}
switch (layer)
{
case TargetLayer.Static:
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
if (!isStaticLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, staticLayer);
isStaticLayerPushed = true;
}
break;
case TargetLayer.Dynamic:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (!isDynamicLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, dynamicLayer);
isDynamicLayerPushed = true;
}
break;
case TargetLayer.None:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
break;
}
if (!draw && isDrawing)
{
d2dRenderTarget.EndDraw();
isDrawing = false;
}
}
So in a class that uses an instance of this DirectX11Renderer class, it starts by setting the background colour without pushing a layer, rendering the static content by clearing the layer then calling several of the draw methods using the static layer as an argument for the enum to push the static layer, then rendering the dynamic content every time a value changes, doing the same thing by using the enum as an argument to pop the static layer and push the dynamic layer, then drawing on it. Finally at the end of the dynamic drawing method I present the frame by using the enum argument of TargetLayer.None to pop all layers, and then present the frame.
What I'm finding is that every time I change the value, (rendering the dynamic content on it's layer, popping the layer then presenting) the screen alternates between showing the content of the dynamic layer on a black background, and all the static and dynamic content as I would want to see it if this were working 100%.
As a minimum working example, here is the code for a form which contains the gauge control:
using System.Windows.Forms;
using System;
namespace SharpDXGauge
{
public partial class Form1 : Form
{
Gauge control;
public Form1()
{
InitializeComponent();
control = new Gauge(100, 0);
control.Dock = DockStyle.Left;
this.Controls.Add(control);
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
control.IndicatorValue = (float)numericUpDown1.Value;
}
}
}
Here is a minimal example of the gauge class (I've removed most of the rendering):
using System;
using System.Windows.Forms;
using System.Diagnostics;
using System.Collections.Generic;
using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.DirectWrite;
using DXGI = SharpDX.DXGI; // Direct X Graphics Infrastructure
using D3D = SharpDX.Direct3D;
using D2D1 = SharpDX.Direct2D1;
using D3D11 = SharpDX.Direct3D11;
using DWrite = SharpDX.DirectWrite;
namespace SharpDXGauge
{
public partial class Gauge : UserControl
{
#region Members
private DirectX11Renderer renderer;
private float max;
private float min;
private float indicatorValue;
private Point topLeft;
private Point bottomRight;
private float height;
private float width;
Point valueTextTopLeft;
Point valueLabelOutlineTopLeft;
Point valueLabelOutlineBottomRight;
#endregion
public Gauge(float max, float min)
{
InitializeComponent();
renderer = new DirectX11Renderer(this);
this.max = max;
this.min = min;
this.indicatorValue = min;
renderer.SetBackgroundColor(renderer.SystemDrawingColorToSharpDXColor(this.BackColor));
DrawStaticContent();
DrawDynamicContent();
}
#region Drawing
private void CalculateSizes()
{
// Static
topLeft = new Point() { x = 25, y = 10 };
bottomRight = new Point() { x = topLeft.x + 20, y = this.Height - 50 };
height = bottomRight.y - topLeft.y;
width = bottomRight.x - topLeft.x;
// Dynamic
string valueText = indicatorValue.ToString();
Size2F valueTextSize = renderer.GetTextSize(valueText, 9.75f);
valueTextTopLeft = new Point() { x = bottomRight.x + 15, y = bottomRight.y - height * indicatorValue / (max - min) - (valueTextSize.Height / 2) };
}
private void DrawStaticContent()
{
CalculateSizes();
renderer.ClearLayer(TargetLayer.Static, Color.Transparent);
renderer.DrawRectangle(TargetLayer.Static, topLeft, bottomRight, Color.Black);
DrawDynamicContent();
renderer.PresentFrame();
}
private void DrawDynamicContent()
{
renderer.ClearLayer(TargetLayer.Dynamic, Color.Transparent);
CalculateSizes();
renderer.DrawText(TargetLayer.Dynamic, indicatorValue.ToString(), Color.White, valueTextTopLeft, 9.75f);
renderer.DrawRoundedRectangle(TargetLayer.Dynamic, valueLabelOutlineTopLeft, valueLabelOutlineBottomRight, 20, Color.Black);
renderer.PresentFrame();
}
#endregion
#region Properties
public float IndicatorValue
{
get
{
return indicatorValue;
}
set
{
if (indicatorValue != value)
{
indicatorValue = value;
DrawDynamicContent();
}
}
}
#endregion
#region Disposal
public new void Dispose()
{
renderer.Dispose();
}
#endregion
}
}
And my DirectX11Renderer class, I've removed all the unnecessary drawing methods etc:
using System;
using System.Windows.Forms;
using System.Diagnostics;
using System.Collections.Generic;
using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.DirectWrite;
using DXGI = SharpDX.DXGI; // Direct X Graphics Infrastructure
using D3D = SharpDX.Direct3D;
using D2D1 = SharpDX.Direct2D1;
using D3D11 = SharpDX.Direct3D11;
using DWrite = SharpDX.DirectWrite;
using DMaths = SharpDX.Mathematics.Interop;
namespace SharpDXGauge
{
public enum TargetLayer
{
Static,
Dynamic,
None
}
public struct Point
{
public float x;
public float y;
}
public partial class DirectX11Renderer : IDisposable
{
private Surface backBuffer;
private D3D11.Device d3dDevice;
private D3D11.Device1 d3dDevice1;
private DXGI.Device2 dxgiDevice2;
private D2D1.Device d2dDevice;
private SwapChain1 swapChain1;
private D2D1.Factory d2dFactory;
private DXGI.Factory2 dxgiFactory2;
private Adapter dxgiAdapter;
private RenderTarget d2dRenderTarget;
private DWrite.Factory dWriteFactory;
private Layer staticLayer;
private Layer dynamicLayer;
SolidColorBrush solidColorBrush;
//RadialGradientBrush radialGradientBrush;
//LinearGradientBrush linearGradientBrush;
private Color lastBackColour;
private bool isDrawing;
private LayerParameters layerParams;
private bool isStaticLayerPushed;
private bool isDynamicLayerPushed;
List<double> renderTimesMsec = new List<double>();
public DirectX11Renderer(Control surfaceToDrawOn)
{
// There seems to be endless ways to set up the different objects required for rendering
// This is the most efficient I've found that get's everything you need for 2d rendering
SwapChainDescription1 description = new SwapChainDescription1()
{
Width = surfaceToDrawOn.Width,
Height = surfaceToDrawOn.Height,
Format = Format.R8G8B8A8_UNorm,
Stereo = false,
SampleDescription = new SampleDescription(1, 0),
Usage = Usage.BackBuffer | Usage.RenderTargetOutput,
BufferCount = 2,
Scaling = Scaling.None,
SwapEffect = SwapEffect.FlipSequential,
};
d3dDevice = new D3D11.Device(DriverType.Hardware, DeviceCreationFlags.BgraSupport);
d3dDevice1 = d3dDevice.QueryInterface<D3D11.Device1>();
dxgiDevice2 = d3dDevice1.QueryInterface<DXGI.Device2>();
d2dDevice = new D2D1.Device(dxgiDevice2);
dxgiAdapter = dxgiDevice2.Adapter;
d2dFactory = new D2D1.Factory(D2D1.FactoryType.SingleThreaded);
dxgiFactory2 = dxgiAdapter.GetParent<DXGI.Factory2>();
swapChain1 = new SwapChain1(dxgiFactory2, d3dDevice1, surfaceToDrawOn.Handle, ref description);
backBuffer = swapChain1.GetBackBuffer<Surface>(0);
d2dRenderTarget = new RenderTarget(d2dFactory, backBuffer,
new RenderTargetProperties(new PixelFormat(Format.R8G8B8A8_UNorm, D2D1.AlphaMode.Premultiplied)));
staticLayer = new Layer(d2dRenderTarget);
dynamicLayer = new Layer(d2dRenderTarget);
dWriteFactory = new DWrite.Factory(DWrite.FactoryType.Shared);
layerParams = new LayerParameters()
{
ContentBounds = RectangleF.Infinite,
Opacity = 1
};
solidColorBrush = new SolidColorBrush(d2dRenderTarget, Color.White);
}
public void PresentFrame()
{
StartDrawingAndSwitchLayer(TargetLayer.None, false);
swapChain1.Present(0, PresentFlags.None);
}
public void SetBackgroundColor(Color colour)
{
StartDrawingAndSwitchLayer(TargetLayer.None);
lastBackColour = colour;
d2dRenderTarget.Clear(colour);
}
public void ClearLayer(TargetLayer layer, Color fillColorAfterClear)
{
StartDrawingAndSwitchLayer(layer);
d2dRenderTarget.Clear(fillColorAfterClear);
}
public void DrawRectangle(TargetLayer layer, Point topLeft, Point bottomRight, Color colour, float strokeWidth = 1)
{
StartDrawingAndSwitchLayer(layer);
solidColorBrush.Color = colour;
d2dRenderTarget.DrawRectangle(new DMaths.RawRectangleF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y), solidColorBrush, strokeWidth);
}
public void DrawRoundedRectangle(TargetLayer layer, Point topLeft, Point bottomRight, float roundingRadius, Color colour, float strokeWidth = 1)
{
StartDrawingAndSwitchLayer(layer);
solidColorBrush.Color = colour;
RoundedRectangle roundedRect = new RoundedRectangle();
roundedRect.Rect = new DMaths.RawRectangleF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
roundedRect.RadiusX = roundingRadius;
roundedRect.RadiusY = roundingRadius;
d2dRenderTarget.DrawRoundedRectangle(roundedRect, solidColorBrush, strokeWidth);
}
public void DrawText(TargetLayer layer, string text, Color colour, Point topLeft, float fontSize, FontWeight weight = FontWeight.Normal, FontStyle style = FontStyle.Normal, string font = "Microsoft Sans Serif")
{
StartDrawingAndSwitchLayer(layer);
solidColorBrush.Color = colour;
TextFormat format = new TextFormat(dWriteFactory, font, weight, style, FontStretch.Normal, fontSize);
Size2F textSize = GetTextSize(text, fontSize, 100, 100);
Point bottomRight = new Point() { x = topLeft.x + (int)Math.Ceiling(textSize.Width), y = topLeft.y + (int)Math.Ceiling(textSize.Height) };
d2dRenderTarget.DrawText(text, format,
new DMaths.RawRectangleF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y),
solidColorBrush);
}
public Size2F GetTextSize(string text, float fontSize, float maxWidth = 100000, float maxHeight = 100000, FontWeight weight = FontWeight.Normal, FontStyle style = FontStyle.Normal, string font = "Microsoft Sans Serif")
{
TextFormat format = new TextFormat(dWriteFactory, font, weight, style, FontStretch.Normal, fontSize);
TextLayout layout = new TextLayout(dWriteFactory, text, format, maxWidth, maxHeight);
return new Size2F(layout.Metrics.Width, layout.Metrics.Height);
}
public Color SystemDrawingColorToSharpDXColor(System.Drawing.Color colour)
{
Color toRtn = new Color(new byte[4] { colour.R, colour.G, colour.B, colour.A, });
return toRtn;
}
private void StartDrawingAndSwitchLayer(TargetLayer layer, bool draw = true)
{
if (draw && !isDrawing)
{
d2dRenderTarget.BeginDraw();
isDrawing = true;
}
switch (layer)
{
case TargetLayer.Static:
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
if (!isStaticLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, staticLayer);
isStaticLayerPushed = true;
}
break;
case TargetLayer.Dynamic:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (!isDynamicLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, dynamicLayer);
isDynamicLayerPushed = true;
}
break;
case TargetLayer.None:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
break;
}
if (!draw && isDrawing)
{
d2dRenderTarget.EndDraw();
isDrawing = false;
}
}
public void Dispose()
{
// Release all resources
backBuffer.Dispose();
d3dDevice.Dispose();
d3dDevice1.Dispose();
dxgiDevice2.Dispose();
d2dDevice.Dispose();
swapChain1.Dispose();
d2dFactory.Dispose();
dxgiFactory2.Dispose();
dxgiAdapter.Dispose();
d2dRenderTarget.Dispose();
staticLayer.Dispose();
dynamicLayer.Dispose();
}
}
}
Putting this together gives a minimal example of what I'm seeing, I've removed code handling form resizing so with this, the bottom half appears black to begin with, but using a numerical updown on the form to update the IndicatorValue property creates the alternating all black/ what I want to see frames.
I've been trying to ebug this for a while, I'm beginning to wonder if it's to do with how I've set up my swap chain? At first I thought it must be how I'm handing my layers, and perhaps somehow every other change and dynamic content render it's not popping the layer and only showing the content of the dynamic layer? But stepping through each update has it pushing and popping the layers correctly as far as I can see, and generally not doing it right throws an exception.
Sorry this is so long,
Joe.
I'm trying to make what I think will be a nice effect for an app - a series of images (think wallpaper) will be constantly scrolling in the background during a view. I started prototyping this in Xamarin.Forms, creating a custom control. Planned on a diagonal translation but started with the most basic approach and still ran into some issues fairly quickly, namely that it is not entirely smooth as it gets a bit choppy here and there (even when using caching and just a 10kb image) and 2) if user executes an action that's more involved it may cause a lag and the images get rendered more closely together than they should be. Is there a way to fix up this approach so that it's as smooth as possible and doesn't interfere (or get interfered with) the other UI elements, or is there a far superior approach for something like this - anyone ever tackle this? Please let me know, thanks.
FlyingImageBackground.cs
public class FlyingImageBackground : ContentView
{
public static readonly BindableProperty FlyingImageProperty =
BindableProperty.Create(nameof(FlyingImage), typeof(ImageSource), typeof(FlyingImageBackground), default(ImageSource), BindingMode.TwoWay, propertyChanged: OnFlyingImageChanged);
public ImageSource FlyingImage
{
get => (ImageSource)GetValue(FlyingImageProperty);
set => SetValue(FlyingImageProperty, value);
}
private AbsoluteLayout canvas;
public FlyingImageBackground()
{
this.canvas = new AbsoluteLayout()
{
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
};
this.canvas.SizeChanged += Canvas_SizeChanged;
Content = this.canvas;
}
~FlyingImageBackground() => this.canvas.SizeChanged -= Canvas_SizeChanged;
private static void OnFlyingImageChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackground)bindable;
control.BringToLife();
}
private void BringToLife()
{
if (this.canvas.Width <= 0 || this.canvas.Height <= 0)
return;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
Device.BeginInvokeOnMainThread(async () =>
{
await SendImageWave();
});
return this.canvas.IsVisible;
});
}
private async Task SendImageWave()
{
var startingX = -100;
var endingX = this.canvas.Width;
if (endingX <= 0)
return;
endingX += 100;
var yPositions = Enumerable.Range(0, (int)this.canvas.Height).Where(x => x % 90 == 0).ToList();
var imgList = new List<CachedImage>();
foreach (var yPos in yPositions)
{
var img = new CachedImage
{
Source = FlyingImage,
HeightRequest = 50
};
imgList.Add(img);
this.canvas.Children.Add(img, new Point(startingX, yPos));
}
await Task.WhenAll(
imgList.Select(x => x.TranslateTo(endingX, 0, 10000)));
//.Concat(imgList.Select(x => x.TranslateTo(startingX, 0, uint.MinValue))));
imgList.ForEach(x =>
{
this.canvas.Children.Remove(x);
x = null;
});
imgList = null;
}
private void Canvas_SizeChanged(object sender, EventArgs e)
{
BringToLife();
}
}
Usage example:
Just put it into a Grid in a ContentPage along with the main content:
e.g.:
<ContentPage.Content>
<Grid>
<controls:FlyingImageBackground FlyingImage="fireTruck.png" />
<StackLayout HorizontalOptions="Center">
<Button
Text="I'm a button!" />
<Label
FontAttributes="Bold,Italic"
Text="You're a good man, old sport!!!"
TextDecorations="Underline" />
</StackLayout>
</Grid>
</ContentPage.Content>
Switched to SkiaSharp and much better results. The animation appears smooth and if the flow gets interrupted the images maintain their appropriate distance. Also realized in the first draft using the built-in Xamarin Animations that I screwed up the check for when to run it; .IsVisible prop will remain true even if the page isn't even on the screen anymore, so in this new version needed to bind to a property that tells me if the page is actually active or not (based on when it gets navigated to and navigated away from) and if not then stop the animation. This is still just handling the horizontal scrolling effect for now. Hope someone else finds it useful, and any other improvements would be welcome, please just comment/post an answer!
[DesignTimeVisible(true)]
public class FlyingImageBackgroundSkia : ContentView
{
public static readonly BindableProperty IsActiveProperty =
BindableProperty.Create(
nameof(IsActive),
typeof(bool),
typeof(FlyingImageBackground),
default(bool),
BindingMode.TwoWay,
propertyChanged: OnPageActivenessChanged);
private SKCanvasView canvasView;
private SKBitmap resourceBitmap;
private Stopwatch stopwatch = new Stopwatch();
// consider making these bindable props
private float percentComplete;
private float imageSize = 40;
private float columnSpacing = 100;
private float rowSpacing = 100;
private float framesPerSecond = 60;
private float cycleTime = 1; // in seconds, for a single column
public FlyingImageBackgroundSkia()
{
this.canvasView = new SKCanvasView();
this.canvasView.PaintSurface += OnCanvasViewPaintSurface;
this.Content = this.canvasView;
string resourceID = "XamarinTestProject.Resources.Images.fireTruck.png";
Assembly assembly = GetType().GetTypeInfo().Assembly;
using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
this.resourceBitmap = SKBitmap.Decode(stream);
}
}
~FlyingImageBackgroundSkia() => this.resourceBitmap.Dispose();
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
private static async void OnPageActivenessChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (FlyingImageBackgroundSkia)bindable;
await control.AnimationLoop();
}
private async Task AnimationLoop()
{
this.stopwatch.Start();
while (IsActive)
{
this.percentComplete = (float)(this.stopwatch.Elapsed.TotalSeconds % this.cycleTime) / this.cycleTime; // always between 0 and 1
this.canvasView.InvalidateSurface(); // trigger redraw
await Task.Delay(TimeSpan.FromSeconds(1.0 / this.framesPerSecond)); // non-blocking
}
this.stopwatch.Stop();
}
private void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
var xPositions = Enumerable.Range(0, info.Width + (int)this.columnSpacing).Where(x => x % (int)this.columnSpacing == 0).ToList();
xPositions.Insert(0, -(int)this.columnSpacing);
var yPositions = Enumerable.Range(0, info.Height + (int)this.rowSpacing).Where(x => x % (int)this.rowSpacing == 0).ToList();
yPositions.Insert(0, -(int)this.rowSpacing);
if (this.resourceBitmap != null)
{
foreach (var xPos in xPositions)
{
var xPosNow = xPos + (this.rowSpacing * this.percentComplete);
foreach (var yPos in yPositions)
{
canvas.DrawBitmap(
this.resourceBitmap,
new SKRect(xPosNow, yPos, xPosNow + this.imageSize, yPos + this.imageSize));
}
}
}
}
}
I have 2 picture objects.
I want both of them to move from right to left.
If one go out of the visible panel, I replace its position to the start point.
So there are always 2 pictures moving on the screen.
If I don't use timer, 2 pictures are painted. But if I use a timer with tick event updating their positions to make them move, there is only 1 picture is shown and it's keep flashing, lagging...
Below is my code so far. I'm not familiar with C#. Appreciate any help. Thank you.
Timer interval = 30;
Form 1:
public partial class Form1 : Form
{
Background bg1 = new Background();
Background bg2 = new Background(800);
public Form1()
{
InitializeComponent();
}
private void flowLayoutPanel1_Paint(object sender, PaintEventArgs e)
{
bg1.paint(e);
bg2.paint(e);
}
private void Timer_Tick(object sender, EventArgs e)
{
bg1.updatePosition();
bg2.updatePosition();
this.Refresh();
}
}
Background:
class Background
{
int bg_width = 800;
int bg_height = 500;
Image bg;
Rectangle wb;
private static int x = 0;
public Background()
{
bg = Properties.Resources.bg;
wb = new Rectangle(x, 0, bg_width, bg_height);
}
public Background(int custom_x)
{
x = custom_x;
bg = Properties.Resources.bg;
wb = new Rectangle(x, 0, bg_width, bg_height);
}
public void paint(PaintEventArgs e)
{
Graphics g = e.Graphics;
g.DrawImage(bg, wb);
}
public void updatePosition()
{
x--;
if (x == -800)
{
x = 801;
}
wb.Location = new Point(x, 0);
}
}
I am currently making a tile-based game in c#, but every time I draw the tiles it uses a lot of CPU, and as the tiles get bigger(if i make the game full screen) it consumes even more.
This is my Tile class:
public class Tiles
{
//PRIVATE :
//variabiles
private int XPosition, YPosition;
private Image Texture;
private bool Colidable;
private int SizeW = 32;
private int SizeH = 32;
private Resizer resizer = new Resizer();
//methods
//PUBLIC :
//variabiles
//methods
//CONSTRUCTOR
public Tiles(int _x,int _y,Image _i, int _sW = 32, int _sH = 32, bool _c = false)
{
XPosition = _x;//set position X
YPosition = _y;//set position Y
SizeW = _sW;
SizeH = _sH;
Texture = resizer.ResizeImage(_i, SizeW, SizeH) ;// set texture
Colidable = _c;//set if the tile is colidable,default : false
resizer = null;
}
//DRAW METHOD
//gets graphics object to draw on, adn draws at the position of the tile
public void Draw(Graphics _g)
{
_g.DrawImage(this.Texture, this.XPosition, this.YPosition);
}
//GET PRIVATE MEBERS
//returns if the tile is colidable
public bool getColidable()
{
return this.Colidable;
}
}
and this is how I draw the tiles:
private void DrawMap(Graphics _g)
{
//CALLS THE DRAW METHOD OF EACH TILE
for (int i = 0; i < MAP_WIDTH; i++)
{
for (int j = 0; j < MAP_HEIGHT; j++)
{
Tile[i, j].Draw(_g);
}
}
}
bool TilesUpdate = false;
private void _Window_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(Color.Black);
if (isGameRunning)
{
DrawMap(e.Graphics);
}
else
{
FullRezolutionBtn.Draw(e.Graphics);
BigRezolutionBtn.Draw(e.Graphics);
NormalRezolutionBtn.Draw(e.Graphics);
}
}
private void Update_Tick(object sender, EventArgs e)
{
Invalidate();
}
I want to mention that the map is 20 x 20 tiles and it's cosuming around 50% of the cpu when it's fullscreen.
As I mentioned in the comments, the direction should be to do less painting. One way is to invalidate and paint portions of the drawing canvas only when something related to that portion changes. Windows itself does such optimization for controls/windows.
Here is an example. Look how Gadget class invalidates it's rectangle when some property changes. Then during the paint, only rectangles that intersect with e.ClipRectange are drawn. This highly reduces the number of the drawing operations.
using System;
using System.Drawing;
using System.Windows.Forms;
namespace Samples
{
class Gadget
{
public readonly Control Canvas;
public Gadget(Control canvas) { Canvas = canvas; }
private Rectangle bounds;
public Rectangle Bounds
{
get { return bounds; }
set
{
if (bounds == value) return;
// NOTE: Invalidate both old and new rectangle
Invalidate();
bounds = value;
Invalidate();
}
}
private Color color;
public Color Color
{
get { return color; }
set
{
if (color == value) return;
color = value;
Invalidate();
}
}
public void Invalidate()
{
Canvas.Invalidate(bounds);
}
public void Draw(Graphics g)
{
using (var brush = new SolidBrush(color))
g.FillRectangle(brush, bounds);
}
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var form = new Form { WindowState = FormWindowState.Maximized };
int rows = 9, cols = 9;
var gadgets = new Gadget[rows, cols];
var rg = new Random();
Color[] colors = { Color.Yellow, Color.Blue, Color.Red, Color.Green, Color.Magenta };
int size = 64;
var canvas = form;
for (int r = 0, y = 8; r < rows; r++, y += size)
for (int c = 0, x = 8; c < cols; c++, x += size)
gadgets[r, c] = new Gadget(canvas) { Color = colors[rg.Next(colors.Length)], Bounds = new Rectangle(x, y, size, size) };
int paintCount = 0, drawCount = 0;
canvas.Paint += (sender, e) =>
{
paintCount++;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
if (e.ClipRectangle.IntersectsWith(gadgets[r, c].Bounds))
{
gadgets[r, c].Draw(e.Graphics);
drawCount++;
}
}
}
form.Text = $"Paint:{paintCount} Draw:{drawCount} of {(long)paintCount * rows * cols}";
};
var timer = new Timer { Interval = 100 };
timer.Tick += (sender, e) =>
{
gadgets[rg.Next(rows), rg.Next(cols)].Color = colors[rg.Next(colors.Length)];
};
timer.Start();
Application.Run(form);
}
}
}
Not sure how your resizer class works. i think there is problem when you re-size every image every time.
Texture = resizer.ResizeImage(_i, SizeW, SizeH) ;// set texture
i would replace above line like this
Texture = _i;// set texture but do not resize image now
at the same time update the Draw Function of Tile like below.
public void Draw(Graphics _g)
{
//now specify the location and size of the image.
_g.DrawImage(Texture , new Rectangle(this.XPosition, this.YPosition, SizeW, SizeH));
}
hopefully it should improve the performance.
if it flicker then you can use Double Buffer
I'm in need of a way to make TextBox appear like a parallelogram but i can't figure out how to do so. I currently have this code:
private void IOBox_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Point cursor = PointToClient(Cursor.Position);
Point[] points = { cursor, new Point(cursor.X + 50, cursor.Y), new Point(cursor.X + 30, cursor.Y - 20),
new Point(cursor.X - 20, cursor.Y - 20) };
Pen pen = new Pen(SystemColors.MenuHighlight, 2);
g.DrawLines(pen, points);
}
But apparently it's not working. Either i misplaced/misused it or i'm not doing something right.
This is the method that i use to add it.
int IOCounter = 0;
private void inputOutput_Click(object sender, EventArgs e)
{
IOBox box = new IOBox();
box.Name = "IOBox" + IOCounter;
IOCounter++;
box.Location = PointToClient(Cursor.Position);
this.Controls.Add(box);
}
Any idea how i can fix it? IOBox is a UserControl made by me which contains a TextBox. Is that rightful to do?
If its possible, you should make your application using WPF. WPF is designed to do exactly what you are trying to do.
However, it can be done in WinForms, though not easily. You will need to make a new class that inherits the TextBox WinForm control. Here is an example that makes a TextBox look like a circle:
public class MyTextBox : TextBox
{
public MyTextBox() : base()
{
SetStyle(ControlStyles.UserPaint, true);
Multiline = true;
Width = 130;
Height = 119;
}
public override sealed bool Multiline
{
get { return base.Multiline; }
set { base.Multiline = value; }
}
protected override void OnPaintBackground(PaintEventArgs e)
{
var buttonPath = new System.Drawing.Drawing2D.GraphicsPath();
var newRectangle = ClientRectangle;
newRectangle.Inflate(-10, -10);
e.Graphics.DrawEllipse(System.Drawing.Pens.Black, newRectangle);
newRectangle.Inflate(1, 1);
buttonPath.AddEllipse(newRectangle);
Region = new System.Drawing.Region(buttonPath);
base.OnPaintBackground(e);
}
}
Keep in mind that you will still have to do other things, such as clipping the text, etc. But this should get you started.