I have a TextBlock. I want to draw inside it (within its current visual boundaries). I need DrawingContext for it.
How do I get DrawingContext?
(MSDN says that any FrameworkElement is descendant from Visual and the Visual provides support for rendering.. But I cant quite find how exactly)
Please note - this code will be called several time a second. I am looking for the most efficient solution (this is why I took on the DrawingContext in first place).
what I did: I wrote my own adorner, adorned the textblock, used OnRender to get DrawingContext, I am writing text directly to the adorner and invalidating Visual at each change.
The boost in performance (measured carefully with Ants profiler) is 4.5 times better than writing text directly to the text block and even more so than binding string property.
Why not overlay the TextBlock on a Canvas and draw to the Canvas ?
XAML
<Grid>
<Canvas Background='Orange'
x:Name='drawingCanvas'
Width='{Binding ActualWidth, ElementName=textblock1, Mode=OneWay}'
Height='{Binding ActualHeight, ElementName=textblock1, Mode=OneWay}' />
<TextBlock Text='Example'
x:Name='textblock1'
HorizontalAlignment='Center'
VerticalAlignment='Center'
FontSize='40' />
</Grid>
CODE
public MainWindow() {
InitializeComponent();
this.Loaded += MainWindow_Loaded;
_timer.Interval = TimeSpan.FromMilliseconds(100);
_timer.Start();
_timer.Tick += _timer_Tick;
}
private void _timer_Tick(object sender, EventArgs e) {
var newX = _bezierSeg.Point1.X + .1;
_bezierSeg.Point1 = new Point(Math.Sin(newX) * 12, 0);
}
private DispatcherTimer _timer = new DispatcherTimer();
private BezierSegment _bezierSeg = new BezierSegment();
private void MainWindow_Loaded(object sender, RoutedEventArgs e) {
var arcPath = new Path();
var figure = new PathFigure();
figure.StartPoint = new Point(0, 0);
var Point1 = new Point(textblock1.ActualHeight, 0);
var Point2 = new Point(textblock1.ActualWidth - 30, textblock1.ActualHeight - 20);
var Point3 = new Point(textblock1.ActualWidth, textblock1.ActualHeight);
_bezierSeg.Point1 = Point1;
_bezierSeg.Point2 = Point2;
_bezierSeg.Point3 = Point3;
var myPathSegmentCollection = new PathSegmentCollection();
myPathSegmentCollection.Add(_bezierSeg);
figure.Segments = myPathSegmentCollection;
var pathCollection = new PathFigureCollection();
pathCollection.Add(figure);
var pathGeometry = new PathGeometry();
pathGeometry.Figures = pathCollection;
arcPath.Stroke = new SolidColorBrush(Colors.Red);
arcPath.Fill = new SolidColorBrush(Colors.Yellow);
arcPath.StrokeThickness = 2;
arcPath.Data = pathGeometry;
drawingCanvas.Children.Add(arcPath);
}
Related
I have a simple map and a square that I would like to move from point A to D, through B and C. I've declared a method Animate:
public void Animate(double[] FirstPoint, double[] SecondPoint, Image img)
{
double x1 = FirstPoint[0];
double x2 = SecondPoint[0];
double y1 = FirstPoint[1];
double y2 = SecondPoint[1];
TranslateTransform trans = new TranslateTransform();
img.RenderTransform = trans;
DoubleAnimation anim1 = new DoubleAnimation(y1, y2, TimeSpan.FromSeconds(1));
DoubleAnimation anim2 = new DoubleAnimation(x1, x2, TimeSpan.FromSeconds(1));
trans.BeginAnimation(TranslateTransform.YProperty, anim1);
trans.BeginAnimation(TranslateTransform.XProperty, anim2);
}
The main problem is that when I use those method like this:
obj.Animate(obj.A, obj.B, Car);
obj.Animate(obj.B, obj.C, Car);
obj.Animate(obj.C, obj.D, Car);
...there's animation shown only from point C to D. When I added a MessageBox.Show to Animate method, it displayed the animation properly.
I feel that I might not fully understand the concept behind using those classes to animate objects. Any thoughts?
You can use this code as the sample for understanding, how to work with multiple animations.
This is full code for MainWindow.xaml.cs.
public partial class MainWindow : Window
{
private const string CarTransform = "CarTransform";
private Image _car;
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Loaded -= MainWindow_Loaded;
// create and add Car image to LayoutRoot grid
_car = new Image();
_car.Source = new BitmapImage(new Uri("/car-icon-hi.png", UriKind.Relative));
_car.Width = 64;
_car.Height = 64;
_car.HorizontalAlignment = System.Windows.HorizontalAlignment.Left;
_car.VerticalAlignment = System.Windows.VerticalAlignment.Top;
_car.Margin = new Thickness(5);
_car.RenderTransform = new TranslateTransform();
LayoutRoot.Children.Add(_car);
}
private DoubleAnimation CreateAnimationFor(double from, double to, TimeSpan? beginTime, string targetName, DependencyProperty propertyPath)
{
DoubleAnimation da = new DoubleAnimation();
da.From = from;
da.To = to;
da.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
if (beginTime != null)
da.BeginTime = beginTime;
Storyboard.SetTargetName(da, targetName);
Storyboard.SetTargetProperty(da, new PropertyPath(propertyPath));
return da;
}
private void StartAnimationClick(object sender, RoutedEventArgs e)
{
TranslateTransform _trans = _car.RenderTransform as TranslateTransform;
this.RegisterName(CarTransform, _trans); // register name for TranslateTransform instance, this action is needed for working a Storyboard with multiple animations
Storyboard sb = new Storyboard();
// from A to B
sb.Children.Add(CreateAnimationFor(0, 100, null, CarTransform, TranslateTransform.XProperty));
sb.Children.Add(CreateAnimationFor(0, 0, null, CarTransform, TranslateTransform.YProperty));
// from B to C
sb.Children.Add(CreateAnimationFor(100, 100, TimeSpan.FromSeconds(1), CarTransform, TranslateTransform.XProperty));
sb.Children.Add(CreateAnimationFor(0, 100, TimeSpan.FromSeconds(1), CarTransform, TranslateTransform.YProperty));
// from C to D
sb.Children.Add(CreateAnimationFor(100, 300, TimeSpan.FromSeconds(2), CarTransform, TranslateTransform.XProperty));
sb.Children.Add(CreateAnimationFor(100, 250, TimeSpan.FromSeconds(2), CarTransform, TranslateTransform.YProperty));
sb.Begin(this);
}
}
Result of this code:
I'm actually trying to add in a asynchronous way some UIElement to my Canvas define in my MainPage.
As I understand the basic way to add UIElement to a Canvas is to add it on his UIElementCollection, for example with a Line I should do like that:
Line line = new Line();
// Line attributes
line.Stroke = new SolidColorBrush(Colors.Purple);
line.StrokeThickness = 15;
Point point1 = new Point();
point1.X = 0;
point1.Y = 0;
Point point2 = new Point();
point2.X = 480;
point2.Y = 720;
line.X1 = point1.X;
line.Y1 = point1.Y;
line.X2 = point2.X;
line.Y2 = point2.Y;
// Line attributes
MyCanvas.Children.Add(line);
Let's imagine that I have a Class call Graphics that needs to access this Canvas in order to draw on it.
public class Graphics
{
public void drawLine()
{
//Using Dispatcher in order to access the main UI thread
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
Line line = new Line();
// Line attributes
/**
* Here I want to access the Canvas of the MainPage
* I have no Idea what to put here !!!
*
**/
});
}
}
In the place "I have no Idea what to put here !!!" I tried to access directly to the MainPage Canvas --> FAIL
I tried to declare a public static UIElementCollection in the MainPage in order ta add my UIElement then pass it to the Canvas but not possible because UIElementCollection has no constructor --> FAIL
But those ideas seems to be dirty coding and not really elegant. So through my research I see that MVVM should do the magic. But all the tutorial I found were doing the data-biding through the xaml file which can't be use in my case.
So I have 2 questions:
First: How is use the UIElementCollection of the Canvas? (is there a hidden method called who draws it, like Paint or Repaint in JAVA?)
Second: If I want to follow the MVVM pattern can I consider the MainPage as my View, the Graphics class as my ViewModel and the UIElement as my Model?
This is a really basic example but should get you going in the correct direction.
Graphics.cs
public class Graphics
{
public ObservableCollection<UIElement> UIElements { get; set; }
int poisiton = 0;
private Timer backgroundTimer;
public Graphics()
{
this.UIElements = new ObservableCollection<UIElement>();
this.backgroundTimer = new Timer(new TimerCallback((timer) => {
Deployment.Current.Dispatcher.BeginInvoke(() => this.GenerateLine());
}), null, 2000, 3000);
}
private void GenerateLine()
{
Line line = new Line();
// Line attributes
line.Stroke = new SolidColorBrush(Colors.Purple);
line.StrokeThickness = 15;
Point point1 = new Point();
point1.X = this.poisiton;
point1.Y = this.poisiton;
Point point2 = new Point();
point2.X = this.poisiton;
point2.Y = this.poisiton + 30;
line.X1 = point1.X;
line.Y1 = point1.Y;
line.X2 = point2.X;
line.Y2 = point2.Y;
// Line attributes
this.poisiton += 10;
UIElements.Add(line);
}
}
MainPage.xaml.cs
public MainPage()
{
InitializeComponent();
this.Loaded += MainPage_Loaded;
// Sample code to localize the ApplicationBar
//BuildLocalizedApplicationBar();
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var graphics = new Graphics();
this.ContentPanel.DataContext = graphics;
}
MainPage.xaml
<Canvas x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ItemsControl ItemsSource="{Binding UIElements}">
</ItemsControl>
</Canvas>
I hope this helps.
I'm doing a minimalist test app after encountering an issue with my real program, using WinForms. I put a small panel (child) inside a bigger panel (parent). The bigger panel has AutoScroll set to true. The child panel has the default Anchors set to Top and Left. The child panel is not docked.
The behavior I want is for scrollbars to appear whenever the smaller panel's location is too offset, either top, bottom, left or right. The problem is that it only works when it's too far right, or too far in the bottom. No scrollbars appear when it's too much in the top or too much in the left directions.
I use two simple buttons to force the child panel's location 200 pixels to the left, or 200 pixels to the right to have a quick way of easily modifying its position.
Here's my Form1() code:
private void button1_Click(object sender, EventArgs e)
{
childPanel.Location = new Point(childPanel.Location.X - 200, childPanel.Location.Y);
hostPanel.Invalidate();
}
private void button2_Click(object sender, EventArgs e)
{
childPanel.Location = new Point(childPanel.Location.X + 200, childPanel.Location.Y);
hostPanel.Invalidate();
}
Here's the designer code:
private void InitializeComponent()
{
this.hostPanel = new System.Windows.Forms.Panel();
this.childPanel = new System.Windows.Forms.Panel();
this.moveChildLeft = new System.Windows.Forms.Button();
this.moveChildRight = new System.Windows.Forms.Button();
this.hostPanel.SuspendLayout();
this.SuspendLayout();
//
// hostPanel
//
this.hostPanel.AutoScroll = true;
this.hostPanel.BackColor = System.Drawing.SystemColors.AppWorkspace;
this.hostPanel.Controls.Add(this.childPanel);
this.hostPanel.Location = new System.Drawing.Point(239, 48);
this.hostPanel.Name = "hostPanel";
this.hostPanel.Size = new System.Drawing.Size(400, 400);
this.hostPanel.TabIndex = 0;
//
// childPanel
//
this.childPanel.BackColor = System.Drawing.SystemColors.ButtonHighlight;
this.childPanel.Location = new System.Drawing.Point(29, 62);
this.childPanel.Name = "childPanel";
this.childPanel.Size = new System.Drawing.Size(342, 259);
this.childPanel.TabIndex = 0;
//
// moveChildLeft
//
this.moveChildLeft.Location = new System.Drawing.Point(61, 81);
this.moveChildLeft.Name = "moveChildLeft";
this.moveChildLeft.Size = new System.Drawing.Size(75, 23);
this.moveChildLeft.TabIndex = 1;
this.moveChildLeft.Text = "Left 200";
this.moveChildLeft.UseVisualStyleBackColor = true;
this.moveChildLeft.Click += new System.EventHandler(this.button1_Click);
//
// moveChildRight
//
this.moveChildRight.Location = new System.Drawing.Point(61, 111);
this.moveChildRight.Name = "moveChildRight";
this.moveChildRight.Size = new System.Drawing.Size(75, 23);
this.moveChildRight.TabIndex = 2;
this.moveChildRight.Text = "Right 200";
this.moveChildRight.UseVisualStyleBackColor = true;
this.moveChildRight.Click += new System.EventHandler(this.button2_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1018, 549);
this.Controls.Add(this.moveChildRight);
this.Controls.Add(this.moveChildLeft);
this.Controls.Add(this.hostPanel);
this.Name = "Form1";
this.Text = "Form1";
this.hostPanel.ResumeLayout(false);
this.ResumeLayout(false);
}
Yet - Another Winforms incapability quickly solved by WPF:
XAML:
<Window x:Class="WpfApplication4.Window3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window3" WindowState="Maximized">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Button Content="Left" Click="MoveLeft"/>
<Button Content="Right" Click="MoveRight"/>
</StackPanel>
<Border BorderBrush="Blue" BorderThickness="1" Width="300" Height="300">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" x:Name="Scr">
<Grid Background="Green" Width="100" Height="100" x:Name="Grid"/>
</ScrollViewer>
</Border>
</DockPanel>
</Window>
Code behind:
using System.Windows;
namespace WpfApplication4
{
public partial class Window3 : Window
{
public Window3()
{
InitializeComponent();
}
private void MoveRight(object sender, RoutedEventArgs e)
{
if (Grid.Margin.Right <= 0)
{
Grid.Margin = new Thickness(Grid.Margin.Left + 100,0,0,0);
}
else
{
Grid.Margin = new Thickness(0, 0, Grid.Margin.Right - 100, 0);
Scr.ScrollToHorizontalOffset(Scr.HorizontalOffset - 100);
}
}
private void MoveLeft(object sender, RoutedEventArgs e)
{
if (Grid.Margin.Left > 0)
{
Grid.Margin = new Thickness(Grid.Margin.Left - 100, 0, 0, 0);
}
else
{
Grid.Margin = new Thickness(0, 0, Grid.Margin.Right + 100, 0);
Scr.ScrollToHorizontalOffset(Scr.HorizontalOffset + 100);
}
}
}
}
Copy and paste my code in a File -> New -> WPF Application and see the results for yourself.
Eventually, you might want to convert your app to WPF. Since, Winform is condemned to a small death.
I saw an example on how to create a glow effect when the image gets focus in mark-up.
Below is the C# code example i found that uses double animation for opacity of a rectangle, i need to perform a glow or swivel effect using C# and not markup since i am not comfortable with it.
DoubleAnimation myDoubleAnimation = new DoubleAnimation();
myDoubleAnimation.From = 1.0;
myDoubleAnimation.To = 0.0;
myDoubleAnimation.Duration = new Duration(TimeSpan.FromSeconds(5));
myDoubleAnimation.AutoReverse = true;
myDoubleAnimation.RepeatBehavior = RepeatBehavior.Forever;
myStoryboard = new Storyboard();
myStoryboard.Children.Add(myDoubleAnimation);
Storyboard.SetTargetName(myDoubleAnimation, myRectangle.Name);
Storyboard.SetTargetProperty(myDoubleAnimation, new PropertyPath(Rectangle.OpacityProperty));
You can add this in your Main(), after InitializeComponent();
textBlock1.Text = "Mouse over me";
var effect = new DropShadowEffect();
effect.Color = Colors.Red;
effect.BlurRadius = 10d;
effect.Opacity = 0d;
effect.ShadowDepth = 0d;
textBlock1.Effect = effect;
textBlock1.MouseEnter += (s, e) => {
var anim = new DoubleAnimation(0d, 1d, new Duration(TimeSpan.FromMilliseconds(500)));
effect.BeginAnimation(DropShadowEffect.OpacityProperty, anim); };
textBlock1.MouseLeave += (s, e) => {
var anim = new DoubleAnimation(1d, 0d, new Duration(TimeSpan.FromMilliseconds(500)));
effect.BeginAnimation(DropShadowEffect.OpacityProperty, anim); };
But I'd suggest stop wasting time with C# for GUI code, XAML is so much more convenient.
I'm designing a game like this
class Anima
{
Storyboard story;
Random rand;
Canvas canvas;
Ellipse target;
public Anima() {
rand = new Random();
canvas = new Canvas();
target = new Ellipse();
target.Fill = Brushes.Red;
target.Width = 50;
target.Height = 50;
Canvas.SetLeft(target, rand.NextDouble() * 300);
Canvas.SetTop(target, rand.NextDouble() * 300);
canvas.Children.Add(target);
story = new Storyboard();
story.BeginTime = TimeSpan.FromMilliseconds(rand.Next(500, 5000));
DoubleAnimation a = new DoubleAnimation();
a.To = rand.NextDouble() * 300;
a.Duration = new Duration(TimeSpan.FromMilliseconds(50));
Storyboard.SetTarget(a, target);
Storyboard.SetTargetProperty(a, new PropertyPath(Canvas.LeftProperty));
story.Children.Add(a);
DoubleAnimation b = new DoubleAnimation();
b.To = rand.NextDouble() * 300;
b.Duration = new Duration(TimeSpan.FromMilliseconds(50));
Storyboard.SetTarget(b, target);
Storyboard.SetTargetProperty(b, new PropertyPath(Canvas.TopProperty));
story.Children.Add(b);
story.Completed += new EventHandler(story_Completed);
Window win = new Window();
win.Loaded += delegate(object sender, RoutedEventArgs e) {
story.Begin();
};
win.Content = canvas;
win.Show();
}
void story_Completed(object sender, EventArgs e) {
int next = rand.Next(500, 5000);
double left = rand.NextDouble() * 300;
double top = rand.NextDouble() * 300;
Console.WriteLine("position: ({0:G6}, {1:G6})", Canvas.GetLeft(target), Canvas.GetTop(target));
Console.WriteLine("state : wait for " + next + " ms");
Console.WriteLine("next : ({0:G6}, {1:G6})", left, top);
Console.WriteLine();
(story.Children[0] as DoubleAnimation).To = left;
(story.Children[1] as DoubleAnimation).To = top;
story.BeginTime = TimeSpan.FromMilliseconds(next);
story.Begin();
}
}
Everything is great, but I found that the ellipse didn't get the right position and make an error about 2% to 50%. It seems like the Storyboard.Completed event triggered before the end of animation.
What's wrong with it?
I would suggest that you use CompositionTarget.Rendering event or DispatcherTimer for controlling complex animations, especially if you are developing a game. The built-in animations are not precise enough and cannot be controlled easily.
Here are some links that will get you started on CompositionTarget
How to: Render on a Per Frame Interval Using CompositionTarget
Using the CompositionTarget
Fun with Animation Part 1
use Dispatcher.BeginInvoke(...), you can get the right value