How do I only repaint a special area? - c#

My English skill is poor because I'm not a native speaker.
I have written an editor that has a visual effect like Highlight.
The editor has DrawingControl for representing a visual effect as the following code.
public class HighlightTextBox : TextBox
{
private DrawingControl renderCanvas;
}
public class DrawingControl : FrameworkElement
{
private VisualCollection visuals;
private DrawingVisual visual;
public DrawingControl()
{
visual = new DrawingVisual();
visuals = new VisualCollection(this);
visuals.Add(visual);
}
public DrawingContext GetContext()
{
return visual.RenderOpen();
}
protected override int VisualChildrenCount
{
get { return visuals.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= visuals.Count)
throw new ArgumentOutOfRangeException();
return visuals[index];
}
}
If a user input a character on the editor then the TextChanged event is called.
And after that, the OnRender function is called by calling InvalidateVisual function.
TextChanged(s,e)
{
InvalidateVisual();
}
Then all text is drawn that has a visual effect by using DrawingContext in the DrawingControl.
protected override void OnRender(DrawingContext drawingContext)
{
if (this.bSingleLineChanged)
{
this.SingleLineChangedRender(drawingContext);
this.bSingleLineChanged = false;
return;
}
else
this.AllRender(drawingContext);
}
private void AllRender(DrawingContext drawingContext)
{
// Calculate drawing position, texts, etc...
var dc = this.renderCanvas.GetContext();
// Here draw texts
dc.Close();
base.OnRender(drawingContext);
}
As a result, drawing process success when all text is repainted but the way take a lot of time.
To shorten a drawing time, I tried to repaint only an updated part.
The part drawing process is defined in the SingleLineChangedRender function.
Also the logic of the function alike with an AllRender function.
Now I would explain a problem.
I calculate an updated part and have written logic in the SingleLineChangedRender function.
But when is called a GetContext function in the SingleLineChangedRender function all drawn context is erased.
private void SingleLineChangedRender(DrawingContext drawingContext)
{
// I think that here all drawn contexts are erased.
var dc = this.renderCanvas.GetContext();
// Here draw texts
dc.Close();
base.OnRender(drawingContext);
}
I want to repaint only an updated part but I can't do it because all contexts is erased when the GetContext function is called.
I would like to seek your advice to solve this problem.
Thank you for reading.

I follow your advice and I solved this problem by updating DrawingControl class.
I updated the TextViewer class as a container of DrawingContext.
As a result, the class has formed the following.
public class TextViewer : FrameworkElement
{
// For highlighting of the selected line.
private DrawingVisual selectionLineAppearance = new DrawingVisual();
// For highlighting tokens of the lines.
private VisualCollection lines;
protected override int VisualChildrenCount
{
// selectionLineAppearance + lines
get { return 1+lines.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= VisualChildrenCount)
throw new ArgumentOutOfRangeException();
if (index == 0) return this.selectionLineAppearance;
return lines[index-1];
}
// logices
...
}
Now, Editor can repaint only an updated part. Addition can paint the appearance of the selected line.
Thank you for your advice.

Related

Transparent picturebox flickers

So I'm coding a project that is Plants vs. Zombies made of pure c# using no game engines and here I have a graphic problem.
I need to render a transparent picturebox over another transparent picturebox and I had to define a new control that is really transparent and everything goes fine with the transparency aspect but here is a problem:
Flicker :|
I have so much of them because of the RecreateHandle(); method I use when I change the image of my control to make animations and when it moves to have the real transparency.
Here is my code I wonder if any one could help !
public class TransparentControl : Control
{
private Image _image;
public TransparentControl()
{
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.SupportsTransparentBackColor, true);
base.BackColor = Color.FromArgb(0, 0, 0, 0);
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ExStyle |= 0x20;
return cp;
}
}
protected override void OnMove(EventArgs e)
{
//RecreateHandle();
}
protected override void OnPaint(PaintEventArgs e)
{
if (_image != null)
{
e.Graphics.DrawImage(_image, (Width / 2) - (_image.Width / 2), (Height / 2) - (_image.Height / 2));
}
}
protected override void OnPaintBackground(PaintEventArgs e)
{
//Do not paint background
}
//Hack
public void Redraw()
{
//RecreateHandle();
}
public Image Image
{
get
{
return _image;
}
set
{
_image = value;
//RecreateHandle();
}
}
}
One way to reduce flickering is to create two different bitmaps. One to draw to, and one to display.
Image BackBuffer;
Image BrontBuffer;
private void RotateImages()
{
lock (this.BackBuffer)
{
var temp = this.BackBuffer;
this.BackBuffer = this.FrontBuffer;
this.FrontBuffer = temp;
}
}
Draw everything into your BackBuffer, then show the FrontBuffer.
Note that you should declare the FrontBuffer with the same width/height values as the BackBuffer, in pretty much exactly the same spot that you declare the BackBuffer.
Use the method RotateImages() immediately after (before should work, too) you display the front buffer.

Getting mouse click in design time, but also let the control be selected at the same time

I am creating a custom control that acts like the TabControl. I want the tab headers to be clickable in design time, just like the TabControl. Using the sample code I found on the Microsoft site, I wrote the following code. It works, but there is one problem.
As for the real TabControl, if you click a tab header when the TabControl has not been selected, the selected tab changes, AND the TabControl gets selected showing the resizing border which has a moving handle and a small button for "TabControl Tasks".
Doing the same thing with my code below only changes the selected tab header; it does not make my control be selected. I think I need to tell the Visual Studio Designer to select my control, somehow. But how?
public class MyDesigner : System.Windows.Forms.Design.ControlDesigner
{
private Adorner MyAdorner;
public override void Initialize(IComponent component)
{
base.Initialize(component);
MyAdorner = new Adorner();
this.BehaviorService.Adorners.Add(MyAdorner);
MyAdorner.Glyphs.Add(new MyGlyph(BehaviorService, (MyTabControl)Control));
}
protected override void Dispose(bool disposing)
{
if (disposing && MyAdorner != null)
{
BehaviorService b = BehaviorService;
if (b != null)
{
b.Adorners.Remove(MyAdorner);
}
}
}
class MyGlyph : Glyph
{
MyTabControl TargetControl;
BehaviorService behaviorSvc;
public MyGlyph(BehaviorService behaviorSvc, MyTabControl targetControl)
:base(new ClickingBehaviour())
{
this.behaviorSvc = behaviorSvc;
this.TargetControl = targetControl;
}
private void OnMouseClick(MouseButtons button, Point adornerPoint)
{
var screenPoint = behaviorSvc.AdornerWindowPointToScreen(adornerPoint);
var targetPoint = TargetControl.PointToClient(screenPoint);
TargetControl.DesignTimeClick(button, targetPoint);
}
public override Rectangle Bounds
{
get
{
Point edge = behaviorSvc.ControlToAdornerWindow(TargetControl);
Size size = TargetControl.Size;
Rectangle bounds = new Rectangle(edge, size);
return bounds;
}
}
public override Cursor GetHitTest(Point p)
{
Cursor handled = null;
if (Bounds.Contains(p))
{
var screenPoint = behaviorSvc.AdornerWindowPointToScreen(p);
var targetPoint = TargetControl.PointToClient(screenPoint);
if(TargetControl.IsHeaderArea(targetPoint))
handled = Cursors.Default;
}
return handled;
}
public override void Paint(PaintEventArgs pe)
{
}
class ClickingBehaviour : Behavior
{
public override bool OnMouseDown(Glyph g, MouseButtons button, Point mouseLoc)
{
MyGlyph myG = g as MyGlyph;
myG.OnMouseClick(button, mouseLoc);
return base.OnMouseDown(g, button, mouseLoc);
}
}
}
}
I could not figure out how to do it with the approach which uses an Adorner as above. So, I resorted to using Win32 message (I tried to avoid using Win32 for interoperability).
protected override void WndProc(ref Message m)
{
if (m.Msg == 0x0201 && m.WParam.ToInt32() == 0x0001)
{
var target = (Control as MyTabControl);
if (true) //how to know if focused?
{
var lparam32 = m.LParam.ToInt32();
int lowWord = lparam32 & 0xffff;
int highWord = lparam32 >> 16;
Point p = new Point(lowWord, highWord);
if (target.IsHeaderArea(p))
{
target.DesignTimeClick(MouseButtons.Left, p);
}
}
}
base.WndProc(ref m);
}
It works, but the only minor problem is that the tab index changes at the first click when the tab is not selected. Clicking a tab on a real TabControl when it is not selected does not change the tab but only selects the tab. But I guess I can live with this minor difference.

wpf rendering synchronization

I have two custom WPF controls (with custom rendering logic) placed on top of each other (transparent background and stuff). At some point i want to re-draw those controls. My first idea was to call:
foreach(var control in Controls)
{
control.InvalidateVisual();
}
But that did not quite work. I mean it does force the rendering of my controls, but visual update does not happen simultaneously. The controls update one by one, which does not look nice and can lead to confusion (in cases where one control displays an updated visual, while the other still displays an old one). The documentation on InvalidateVisual method is really poor, but i guess it is an async method, which is why i am having this issue.
So the question is: is there a way to synchronize rendering of multiple controls? Apart from creating one mega control containing all the rendering logic (i would like to avoid that).
You generally shouldn't use InvalidateVisual().
To update a control which is staying the same size, you can create a DrawingGroup and put that into the visual-tree during OnRender() -- then you can update the drawing group anytime you like using DrawingGroup.Open(), and WPF will update the UI.
For your case, this would look something like:
class Control1 : UIElement {
DrawingGroup backingStore = new DrawingGroup();
protected override void OnRender(DrawingContext drawingContext) {
base.OnRender(drawingContext);
Render(); // put content into our backingStore
drawingContext.DrawDrawing(backingStore);
}
private void Render() {
var drawingContext = backingStore.Open();
Render(drawingContext);
drawingContext.Close();
}
private void Render(DrawingContext) {
// put your existing drawing commands here.
}
}
class Control2 : UIElement {
DrawingGroup backingStore = new DrawingGroup();
protected override void OnRender(DrawingContext drawingContext) {
base.OnRender(drawingContext);
Render(); // put content into our backingStore
drawingContext.DrawDrawing(backingStore);
}
private void Render() {
var drawingContext = backingStore.Open();
Render(drawingContext);
drawingContext.Close();
}
private void Render(DrawingContext) {
// put your existing drawing commands here.
}
}
class SomeOtherClass {
public void SomeOtherMethod() {
control1.Render();
control2.Render();
}
}
You need to suspend refreshing control until you finish invalidating all of them one way is like this :
class DrawingControl
{
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
private const int WM_SETREDRAW = 11;
public static void SuspendDrawing( Control parent )
{
SendMessage(parent.Handle, WM_SETREDRAW, false, 0);
}
public static void ResumeDrawing( Control parent )
{
SendMessage(parent.Handle, WM_SETREDRAW, true, 0);
parent.Refresh();
}
}
see here for other ways : How do I suspend painting for a control and its children?
Another approach is to use data-binding to trigger the rendering if control has some Data property that is bound to viewmodel.
public static readonly DependencyProperty DataProperty
= DependencyProperty.Register("Data", typeof(DataType), typeof(MyView),
new FrameworkPropertyMetadata(default(DataType),
FrameworkPropertyMetadataOptions.AffectsRender));
public DataType Data
{
get { return (DataType)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
Note the AffectsRender flag. Then you can re-draw all controls simultaneously by simultaneously updating bound properties:
foreach(var viewModel in ViewModels)
{
viewModel.Data = ...;
}

How do I set the background image to None, or some other default value, in an image button?

I'm building off this question and creating a simple ImageButton class that represents a button containing only an image. I implemented (at least I believe I did) the suggestions in this answer, but I'm still getting an exception with this code:
public class ImageButton : Button
{
// Overrides the property
public override Image BackgroundImage
{
get { return base.BackgroundImage; }
set
{
base.BackgroundImage = value;
if (value != null) this.Size = value.Size;
}
}
// Shadows the property (note the -new- keyword)
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Size Size
{
get
{
return base.Size;
}
set
{
base.Size = value;
}
}
public ImageButton()
{
this.BackgroundImage = base.BackgroundImage;
this.BackgroundImageChanged += new EventHandler(ImageButton_BackgroundImageChanged);
}
void ImageButton_BackgroundImageChanged(object sender, EventArgs e)
{
this.Size = this.BackgroundImage.Size;
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawImage(BackgroundImage, 0, 0); // <-- Error occurs here
}
protected override void OnPaintBackground(PaintEventArgs e)
{
// Do nothing
}
}
When I try to add this control to the designer, I get
The control ImageButton has thrown an unhandled exception in the
designer and has been disabled.
Exception: Value cannot be null. Parameter name: image
Stack trace: ImageButton.OnPaint(PaintEventArgs e) in
ImageButton.cs:line48
Line 48 is this line:
e.Graphics.DrawImage(BackgroundImage, 0, 0);
I realise that this error is thrown because BackgroundImage is not set to a value, but I'm unsure how to do so in the code. In the actual application, this class will never be added to the designer, but rather added programmatically. How can I fix this exception?
this.BackgroundImage = base.BackgroundImage;
Yes, sure, guaranteed exception. You hope that somebody has set the BackgroundImage property before the constructor runs. That's not possible, the constructor runs before any property on the control can be set.
The next thing that goes wrong is that the Paint event will be raised in the designer as well. Which happens immediately after you drop the control on the form. That's a Kaboom, neither the user nor your code has given the BackgroundImage property a value yet. So just fix the method:
protected override void OnPaint(PaintEventArgs e)
{
if (BackgroundImage != null) e.Graphics.DrawImage(BackgroundImage, 0, 0);
}
to remove the background image just write:
this.BackgroundImage = null;
I don't know C#, but my guess for this would be to have a transparent 1x1px image stored somewhere and assign that on init. That way, there'd be an image but nothing will be seen.
Or you could try adding some check around line 48, like this:
if(BackgroundImage!=null){
e.Graphics.DrawImage(BackGroundImage,0,0);
}
Maybe add something like this:
else{ //No background image
//Draw some dummy/placeholder
}
Yet another method would be to encapsulate the DrawImage call within a try{} block and catch the resulting expression. You should be able to safely use an empty catch{}block if it's that one.
What if you don't draw when there is no image set, also call base in the beginning of the function.
base.OnPaint(e);
There is a lot of redundant code in your implementation: try this revised version...
public class ImageButton : Button
{
public ImageButton()
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.UserPaint, true);
}
protected override void OnPaint(PaintEventArgs pevent)
{
//base.OnPaint(pevent); <-- NO LONGER NEEDED (WE DO NOT WANT THE SYSTEM TO PAINT THE BUTTON);
if (this.BackgroundImage != null)
{
pevent.Graphics.DrawImage(this.BackgroundImage, 0, 0);
}
else
{
//Just fill in black (for example)
pevent.Graphics.FillRectangle(new SolidBrush(Color.Black), this.ClientRectangle);
}
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
base.OnPaintBackground(pevent);
pevent.Graphics.FillRectangle(new SolidBrush(this.BackColor), this.ClientRectangle);
}
public override Image BackgroundImage
{
get
{
return base.BackgroundImage;
}
set
{
base.BackgroundImage = value;
this.Size = base.BackgroundImage.Size;
this.Refresh();
}
}
}

How to limit item position into Canvas?

[EDIT]
Alright,
I edited this post since the code I posted back then had no real links with what I'm trying to do now, but the question is the same.
When I'm talking about limiting objects to a Canvas it was more like a Mouse Clipping, but as I read on many threads, this feature doesn't exist in SL. So I searched a bit around all forums and got this link. But I wasn't able to reproduce it all. Here is the code that is used for the Drag&Drops Events:
public class RoomImage : ContentControl
{
public RoomImage()
{
DefaultStyleKey = typeof(RoomImage);
}
public static readonly DependencyProperty BackgroundImageProperty = DependencyProperty.Register("Source", typeof(ImageSource), typeof(RoomImage), null);
public ImageSource BackgroundImage
{
get { return (ImageSource)GetValue(BackgroundImageProperty); }
set { SetValue(BackgroundImageProperty, value); }
}
//Instance Drag variable
private FrameworkElement _translateZone;
bool _isDrag;
Point StartingDragPoint;
public double Top
{
get { return (double)GetValue(Canvas.TopProperty); }
set { SetValue(Canvas.TopProperty, value); }
}
public double Left
{
get { return (double)GetValue(Canvas.LeftProperty); }
set { SetValue(Canvas.LeftProperty, value); }
}
//Instance Drag events
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_translateZone = GetTemplateChild("PART_TranslateZone") as FrameworkElement;
DefineDragEvents();
}
private void DefineDragEvents()
{
if (_translateZone != null)
{
_translateZone.MouseLeftButtonDown += new MouseButtonEventHandler(translateZone_MouseLeftButtonDown);
_translateZone.MouseLeftButtonUp += new MouseButtonEventHandler(translateZone_MouseLeftButtonUp);
_translateZone.MouseMove += new MouseEventHandler(translateZone_MouseMove);
}
}
private void translateZone_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_isDrag = true;
//start the drag
FrameworkElement DragBar = (FrameworkElement)sender;
DragBar.CaptureMouse();
// drag starting point
StartingDragPoint = e.GetPosition(this);
}
private void translateZone_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
FrameworkElement translateZone = (FrameworkElement)sender;
translateZone.ReleaseMouseCapture();
_isDrag = false;
}
private void translateZone_MouseMove(object sender, MouseEventArgs e)
{
if (_isDrag)
{
UIElement ui = (UIElement)this.Parent;
Point Point = e.GetPosition(ui);
Move(Point.X - StartingDragPoint.X, Point.Y - StartingDragPoint.Y);
}
}
public void Move(double left, double top)
{
Left = left;
Top = top;
}
}
I found this part of code in a tutorial where they didn't explain the Mouse.Clip at all. I can understand it and reuse it, but I have no clue where I could set the limits. The Parent of this item is a Canvas by the way.
If anyone can provide me some sort of code, or where I should implement mine it would be great!
Thank you, Ephismen.
A Canvas has no size as far as its children are concerned. It is just a relative starting point for rendering. A fixed canvas size is only relevant to the parent of the Canvas.
If you mean the objects are being drawn outside the canvas rectangle, then that is the correct behaviour for a canvas.
To stop objects being drawn outside a canvas you need to set a clipping rectangle in the Clip property of the Canvas.
Update:
Here is a very nice example here of how to have a ClipToBounds attached property. That is definitely the easiest way to implement bounds clipping I have seen.
Another update:
So you want to just keep the child items within the parent canvas. If your items vary in size & shape that is basically collision testing with the sides and cap the min/max left/top values. How complex are the shapes you are dropping? Rectangles are obviously very easy to calculate (as are circles).

Categories

Resources