I've got a few custom controls that are covered in images, and if they get dragged off screen and back on, the image isn't repainted properly. I've got on paint overriden for these various controls, and they seem to work fine, other than they don't get drawn properly if dragged off screen on and on again. Anyone know why this might happen and/or a solution?
Edit: there seem to be issues with some of them even if the dialog is simply moved resized too fast, rather than just if it's taken off screen. They start looking like they've been drawn on top of themselves. Which kind of makes sense, but I can't figure out how to cure it.
Edit 2: These are custom buttons with fourstates (hover, click, normal, disabled), so I don't think the container thing is the issue I don't think..? The OnPaint code is:
private void CQPButton_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(BackColor);
if (BackgroundImage != null)
e.Graphics.DrawImage(BackgroundImage, e.ClipRectangle);
if (Image != null)
{
RectangleF rect = new RectangleF();
rect.X = (float)((e.ClipRectangle.Width - Image.Size.Width) / 2.0);
rect.Y = (float)((e.ClipRectangle.Height - Image.Size.Height) / 2.0);
rect.Width = Image.Width;
rect.Height = Image.Height;
e.Graphics.DrawImage(Image, rect);
}
if (Text != null)
{
SizeF size = e.Graphics.MeasureString(this.Text, this.Font);
// Center the text inside the client area of the PictureButton.
e.Graphics.DrawString(this.Text,
this.Font,
new SolidBrush(this.ForeColor),
(this.ClientSize.Width - size.Width) / 2,
(this.ClientSize.Height - size.Height) / 2);
}
}
I've tried forcing repaint on various events, LocationChanged and Move to try and deal with the resize issue, ClientSizeChanged to try and deal with when they're off screen, and nothing is sticking. I don't know what I'm missing...
I completely changed my answer after seeing the code snippet. It has a bug:
RectangleF rect = new RectangleF();
rect.X = (float)((e.ClipRectangle.Width - Image.Size.Width) / 2.0);
rect.Y = (float)((e.ClipRectangle.Height - Image.Size.Height) / 2.0);
Using e.ClipRectangle is incorrect here, it is a value that always changes, depending on what part of the control needs to be repainted. And yes, it changes most when you resize the control or drag it partially off the screen. You need to use the actual size of your control:
rect.X = (float)((this.ClientSize.Width - Image.Size.Width) / 2.0);
rect.Y = (float)((this.ClientSize.Height - Image.Size.Height) / 2.0);
Related
My problem effectively boils down to accurate mouse movement detection.
I need to create my own implementation of an InkCanvas and have succeeded for the most part, except for drawing strokes accurately.
void OnMouseMove(object sneder, MouseEventArgs e)
{
var position = e.GetPosition(this);
if (!Rect.Contains(position))
return;
var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));
DrawBrush.Draw(intPosition, PixelDisplay);
UpdateStroke(intPosition); // calls CaptureMouse
}
This works. The Bitmap (PixelDisplay) is updated and all is well. However, any kind of quick mouse movement causes large skips in the drawing. I've narrowed down the problem to e.GetPosition(this), which blocks the event long enough to be inaccurate.
There's this question which is long beyond revival, and its answers are unclear or simply don't have a noticeable difference.
After some more testing, the stated solution and similar ideas fail specifically because of e.GetPosition.
I know InkCanvas uses similar methods after looking through the source; detect the device, if it's a mouse, get its position and capture. I see no reason for the same process to not work identically here.
I ended up being able to partially solve this.
var position = e.GetPosition(this);
if (!Rect.Contains(position))
return;
if (DrawBrush == null)
return;
var ratio = new Point(Width / PixelDisplay.Size.X, Height / PixelDisplay.Size.Y);
var intPosition = new IntVector(Math2.FloorToInt(position.X / ratio.X), Math2.FloorToInt(position.Y / ratio.Y));
// Calculate pixel coordinates based on the control height
var lastPoint = CurrentStroke?.Points.LastOrDefault(new IntVector(-1, -1));
// Uses System.Linq to grab the last stroke, if it exists
PixelDisplay.Lock();
// My special locking mechanism, effectively wraps Bitmap.Lock
if (lastPoint != new IntVector(-1, -1)) // Determine if we're in the middle of a stroke
{
var alphaAdd = 1d / new IntVector(intPosition.X - lastPoint.Value.X, intPosition.Y - lastPoint.Value.Y).Magnitude;
// For some interpolation, calculate 1 / distance (magnitude) of the two points.
// Magnitude formula: Math.Sqrt(Math.Pow(X, 2) + Math.Pow(Y, 2));
var alpha = 0d;
var xDiff = intPosition.X - lastPoint.Value.X;
var yDiff = intPosition.Y - lastPoint.Value.Y;
while (alpha < 1d)
{
alpha += alphaAdd;
var adjusted = new IntVector(
Math2.FloorToInt((position.X + (xDiff * alpha)) / ratio.X),
Math2.FloorToInt((position.Y + (yDiff * alpha)) / ratio.Y));
// Inch our way towards the current intPosition
DrawBrush.Draw(adjusted, PixelDisplay); // Draw to the bitmap
UpdateStroke(intPosition);
}
}
DrawBrush.Draw(intPosition, PixelDisplay); // Draw the original point
UpdateStroke(intPosition);
PixelDisplay.Unlock();
This implementation interpolates between the last point and the current one to fill in any gaps. It's not perfect when using a very small brush size for example, but is a solution nonetheless.
Some remarks
IntVector is a lazily implemented Vector2 by me, just using integers instead.
Math2 is a helper class. FloorToInt is short for (int)MathF.Round(...))
My linear algebra is weak. WPF is a great system for Rendering different transformations upon an image. However, the standard ScaleTransform will only scale the image's along the x-y axes. When the edges have first been rotated, the result of applying the ScaleTransform will result in a skewed transformation (as shown below) since the edges are no longer aligned.
So, if I have an image that has undergone several different transforms with the result being shown by the WPF rendering system, how do I calculate the correct matrix transform to take the (final rotated image) and scale it along the axes of the rendered image?
Any help or suggestions will be most appreciated.
TIA
(For the complete code, please see my previous question.)
Edit #1: To see the above effect:
Drop image onto Inkcavas. -- no skewing seen.
Rotate image counterclockwise (to about 45deg) -- no skewing seen.
Make the image larger (about twice its prescaled size -- no skewing seen.
Rotate the image clockwise (about back to where it started) -- skewing is
immediately seen during and after the rotation.
If step 3 is skipped, simple rotation -- no matter how many times done -- will not cause the skewing effect. Actually, this makes sense. The ScaleTransform preserves the distance from center from the edges of the image. If the image is at an angle, the x-y distance from the edges of the transform are no longer constant through the width and length of the rendered image. So the edges get appropriately scaled, but the angles are changed.
Here is the most relevant code:
private ImageResizing(Image image)
{
if (image == null)
throw new ArgumentNullException("image");
_image = image;
TransformGroup tg = new TransformGroup();
image.RenderTransformOrigin = new Point(0.5, 0.5); // All transforms will be based on the center of the rendered element.
tg.Children.Add(image.RenderTransform); // Keeps whatever transforms have already been applied.
image.RenderTransform = tg;
_adorner = new MyImageAdorner(image); // Create the adorner.
InstallAdorner(); // Get the Adorner Layer and add the Adorner.
}
Note: The image.RenderTransformOrigin = new Point(0.5, 0.5) is set to the center
of the rendered image. All transforms will be based on the center of the image at the time it is seem by the transform.
public MyImageAdorner(UIElement adornedElement)
: base(adornedElement)
{
visualChildren = new VisualCollection(this);
// Initialize the Movement and Rotation thumbs.
BuildAdornerRotate(ref moveHandle, Cursors.SizeAll);
BuildAdornerRotate(ref rotateHandle, Cursors.Hand);
// Add handlers for move and rotate.
moveHandle.DragDelta += new DragDeltaEventHandler(moveHandle_DragDelta);
moveHandle.DragCompleted += new DragCompletedEventHandler(moveHandle_DragCompleted);
rotateHandle.DragDelta += new DragDeltaEventHandler(rotateHandle_DragDelta);
rotateHandle.DragCompleted += new DragCompletedEventHandler(rotateHandle_DragCompleted);
// Initialize the Resizing (i.e., corner) thumbs with specialized cursors.
BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
// Add handlers for resizing.
topLeft.DragDelta += new DragDeltaEventHandler(TopLeft_DragDelta);
topLeft.DragCompleted += TopLeft_DragCompleted;
// Put the outline border arround the image. The outline will be moved by the DragDelta's
BorderTheImage();
}
#region [Rotate]
/// <summary>
/// Rotate the Adorner Outline about its center point. The Outline rotation will be applied to the image
/// in the DragCompleted event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void rotateHandle_DragDelta(object sender, DragDeltaEventArgs e)
{
// Get the position of the mouse relative to the Thumb. (All cooridnates in Render Space)
Point pos = Mouse.GetPosition(this);
// Render origin is set at center of the adorned element. (all coordinates are in rendering space).
double CenterX = AdornedElement.RenderSize.Width / 2;
double CenterY = AdornedElement.RenderSize.Height / 2;
double deltaX = pos.X - CenterX;
double deltaY = pos.Y - CenterY;
double angle;
if (deltaY.Equals(0))
{
if (!deltaX.Equals(0))
angle = 90;
else
return;
}
else
{
double tan = deltaX / deltaY;
angle = Math.Atan(tan); angle = angle * 180 / Math.PI;
}
// If the mouse crosses the vertical center,
// find the complementary angle.
if (deltaY > 0)
angle = 180 - Math.Abs(angle);
// Rotate left if the mouse moves left and right
// if the mouse moves right.
if (deltaX < 0)
angle = -Math.Abs(angle);
else
angle = Math.Abs(angle);
if (double.IsNaN(angle))
return;
// Apply the rotation to the outline. All Transforms are set to Render Center.
rotation.Angle = angle;
rotation.CenterX = CenterX;
rotation.CenterY = CenterY;
outline.RenderTransform = rotation;
}
/// Rotates image to the same angle as outline arround the render origin.
void rotateHandle_DragCompleted(object sender, DragCompletedEventArgs e)
{
// Get Rotation Angle from outline. All element rendering is set to rendering center.
RotateTransform _rt = outline.RenderTransform as RotateTransform;
// Add RotateTransform to the adorned element.
TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;
RotateTransform rT = new RotateTransform(_rt.Angle);
gT.Children.Insert(0, rT);
AdornedElement.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // clear transform from outline.
}
#endregion //Rotate
#region [TopLeft Corner
// Top Left Corner is being dragged. Anchor is Bottom Right.
void TopLeft_DragDelta(object sender, DragDeltaEventArgs e)
{
ScaleTransform sT = new ScaleTransform(1 - e.HorizontalChange / outline.ActualWidth, 1 - e.VerticalChange / outline.ActualHeight,
outline.ActualWidth, outline.ActualHeight);
outline.RenderTransform = sT; // This will immediately show the new outline without changing the Image.
}
/// The resizing outline for the TopLeft is based on the bottom right-corner. The resizing transform for the
/// element, however, is based on the render origin being in the center. Therefore, the Scale transform
/// received from the outling must be recalculated to have the same effect--only from the rendering center.
///
/// TopLeft_DragCompleted resize the element rendering.
private void TopLeft_DragCompleted(object sender, DragCompletedEventArgs e)
{
// Get new scaling from the Outline.
ScaleTransform _sT = outline.RenderTransform as ScaleTransform;
scale.ScaleX *= _sT.ScaleX; scale.ScaleY *= _sT.ScaleY;
Point Center = new Point(AdornedElement.RenderSize.Width/2, AdornedElement.RenderSize.Height/2);
TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;
ScaleTransform sT = new ScaleTransform( _sT.ScaleX, _sT.ScaleY, Center.X, Center.Y);
gT.Children.Insert(0, sT);
AdornedElement.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // Clear outline transforms. (Same as null).
}
#endregion
Note: I am adding each new transform to the first of the children list. This makes calculations on the image easier.
I could not find with Google or in text all the elements needed to answer this question completely. So, for all other newbies like my self, I will post this (very long) answer. (Editors and Gurus please feel free to correct).
A word toward setup. I have an inkcanvas onto which an image is dropped and added as a child of the inkcanvas. At the time of the drop, an adorner containing a Thumb on each corner for resizing, a Top-Middle thumb for rotating, and a middle thumb for translation is added for final positioning of the image. Along with a "outline" designed as a path element, the Thumbs and outline complete the Adorner and create a kind of wire frame around the adorned element.
There are multiple key points:
WPF first uses a layout pass to position elements within their parent container, followed by a rendering pass to arrange the element. Transforms can be applied to either or both the layout and rendering passes. However, it needs to be noted that the layout pass uses an x-y coordinate system with the origin on the top left of the parent, where as the rendering system inherently references the top left of the child element. If the layout position of the dropped element is not specifically defined, it will by default be added to the "origin" of the parent container.
The RenderTransform is by default a MatrixTransform but can be replaced by a TransformGroup. Using either or both of these allows for Matrices (in the MatrixTransform) or Transforms (in the TransformGroup) to be applied in any order. My preference was to use the MatrixTransforms to better see the relationship between scaling, rotation, and translation.
The rendering of the adorner follows the element it adorns. That is, the element's rendering will also be applied to the Adorner. This behavior can be overriden by use of
public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
As noted in the initial question, I had avoided using SetTop() and SetLeft() as they messed up my other matrices. In hindsight, the reason my matrices failed was because SetTop() and SetLeft() apparently work during the layout phase--so all my coordinates for rendering were off. (I was using a TransalateTransform to position the image upon drag-drop.) However, using SetTop() and SetLeft() apparently act during the Layout Phase. Using this greatly simplified the calculations needed for the Rendering Phase since all coordinates could refer to the image without regard to the position on the canvas.
private void IC_Drop(object sender, DragEventArgs e)
{
InkCanvas ic = sender as InkCanvas;
// Setting InkCanvasEditingMode.None is necessary to capture DrawingLayer_MouseDown.
ic.EditingMode = InkCanvasEditingMode.None;
ImageInfo image_Info = e.Data.GetData(typeof(ImageInfo)) as ImageInfo;
if (image_Info != null)
{
// Display enlarged image on ImageLayer
// This is the expected format for the Uri:
// ImageLayer.Source = new BitmapImage(new Uri("/Images/Female - Front.png", UriKind.Relative));
// Source = new BitmapImage(image_Info.Uri);
Image image = new Image();
image.Width = image_Info.Width * 4;
// Stretch.Uniform keeps the Aspect Ratio but totally screws up resizing the image.
// Stretch.Fill allows for resizing the Image without keeping the Aspect Ratio.
image.Stretch = Stretch.Fill;
image.Source = new BitmapImage(image_Info.Uri);
// Position the drop. Note that SetLeft and SetTop are active during the Layout phase of the image drop and will
// be applied before the Image hits its Rendering stage.
Point position = e.GetPosition(ic);
InkCanvas.SetLeft(image, position.X);
InkCanvas.SetTop(image, position.Y);
ic.Children.Add(image);
ImageResizing imgResize = ImageResizing.Create(image);
}
}
Since I want to be able to resize the image from any direction, the image is set with Stretch.Fill. When Stretch.Uniform was used, the image appeared to first be resized then jump back to its initial size.
Since I am using MatrixTransform, the order of the Matrices is important. So when applying the Matrices, for my use
// Make new render transform. The Matrix order of multiplication is extremely important.
// Scaling should be done first, followed by (skewing), rotation and translation -- in
// that order.
MatrixTransform gT = new MatrixTransform
{
Matrix = sM * rM * tM
};
ele.RenderTransform = gT;
Scaling (sM), is performed before Rotation (rM). Translation is applied last. (C# does matrix multiplication from left to right).
In review of the matrices, it is apparent that the Rotation Matrix also involves skewing elements. (Which makes sense since apparently the RotationTransform is intended to keep the angles at the edges constant). Thus, the Rotation Matrix depends on the size of the image.
In my case, the reason scaling after rotation was causing skewing is because the Scaling transform multiplies the distance between points of the image and the x-y axes. So if the edge of the image is not of constant distance to the x-y axes, scaling will distort (i.e., skew) the image.
Putting this together, results in the following method to resize the image:
Action<Matrix, Vector> DragCompleted = (growthMatrix, v) =>
{
var ele = AdornedElement;
// Get the change vector. Transform (i.e, Rotate) change vector into x-y axes.
// The Horizontal and Vertical changes give the distance between the the current cursor position
// and the Thumb.
Matrix m = new Matrix();
m.Rotate(-AngleDeg);
Vector v1 = v * m;
// Calculate Growth Vector.
var gv = v1 * growthMatrix;
// Apply new scaling along the x-y axes to obtain the rendered size.
// Use the current Image size as the reference to calculate the new scaling factors.
var scaleX = sM.M11; var scaleY = sM.M22;
var W = ele.RenderSize.Width * scaleX; var H = ele.RenderSize.Height * scaleY;
var sx = 1 + gv.X/ W; var sy = 1 + gv.Y / H;
// Change ScalingTransform by applying the new scaling factors to the existing scaling transform.
// Do not add offsets to the scaling transform matrix as they will be included in future scalings.
// With RenderTransformOrigin set to the image center (0.5, 0.5), scalling occurs from the center out.
// Move the new center of the new resized image to its correct position such that the image's thumb stays
// underneath the cursor.
sM.Scale(sx, sy);
tM.Translate(v.X / 2, v.Y / 2);
// New render transform. The order of the transform's is extremely important.
MatrixTransform gT = new MatrixTransform
{
Matrix = sM * rM * tM
};
ele.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // clear this transform from the outline.
};
Just to be clear, my "Growth Matrix" is defined in such a manner as to result in "Positive" growth as the cursor is moved away from the center of the image. For Example, the TopLeft corner will "grow" the image when moved to the left and up. Hence
growth matrix = new Matrix(-1, 0, 0, -1, 0, 0) for top-left corner.
The last problem is that of correctly calculating the rotation center (i.e., I want to spin, not orbit). This becomes greatly simplified by using
// All transforms will be based on the center of the rendered element.
AdornedElement.RenderTransformOrigin = new Point(0.5, 0.5);
Lastly, since I am scaling from a corner, the center of the image needs to be translated to keep the corner underneath the cursor.
Sorry for the length of this answer, but there is much to cover (and learn :) ). Hope this helps somebody.
I am trying to draw multiple lines on a winforms panel using it's graphics object in paint event. I am actually drawing a number of lines joining given points. So, first of all I did this,
private void panel1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawLines(new Pen(new SolidBrush(Color.Crimson), 3), PointFs.ToArray());
float width = 10;
float height = 10;
var circleBrush = new SolidBrush(Color.Crimson);
foreach (var point in PointFs)
{
float rectangleX = point.X - width / 2;
float rectangleY = point.Y - height / 2;
var r = new RectangleF(rectangleX, rectangleY, width, height);
e.Graphics.FillEllipse(circleBrush, r);
}
}
Which produces a result like the image below,
As you can see lines are drawn with having a little bit of extension at sharp turns, which is not expected. So, I changed the drawlines code to,
var pen = new Pen(new SolidBrush(Color.Crimson), 3);
for (int i = 1; i < PointFs.Count; i++)
{
e.Graphics.DrawLine(pen, PointFs[i - 1], PointFs[i]);
}
And now the drawing works fine.
Can anyone tell the difference between the two approaches?
I have just had the same problem (stumbled upon this question during my research), but I have now found the solution.
The problem is caused by the LineJoin property on the Pen used. This DevX page explains the different LineJoin types (see Figure 1 for illustrations). It seems that Miter is the default type, and that causes the "overshoot" when you have sharp angles.
I solved my problem by setting the LineJoin property to Bevel:
var pen = new Pen(new SolidBrush(Color.Crimson), 3);
pen.LineJoin = Drawing2D.LineJoin.Bevel;
Now DrawLines no longer overshoot the points.
I'm looking to create a scrolling surfacelistbox which automatically snaps into a position after a drag is finished so that the center item on the screen is centered itself in the viewport.
I've gotten the center item, but now as usual the way that WPF deals with sizes, screen positions, and offsets has me perplexed.
At the moment I've chosen to subscribe to the SurfaceScrollViewer's ManipulationCompleted event, as that seems to consistently fire after I've finished a scroll gesture (whereas the ScrollChanged event tends to fire early).
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus to a dummy element
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item) && (List.Items[i] as string != "Dummy"))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
int centerItemIdx = visibleElements.Count / 2;
FrameworkElement centerItem = visibleElements[centerItemIdx];
double center = ss.ViewportWidth / 2;
//ss is the SurfaceScrollViewer
Point itemPosition = centerItem.TransformToAncestor(ss).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset + (center - itemPosition.X);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus(); //this also doesn't seem to work, but whatever.
}
The list snaps, but where it snaps seems to be somewhat chaotic. I have a line down the center of the screen, and sometimes it looks right down the middle of the item, but other times it's off to the side or even between items. Can't quite nail it down, but it seems that the first and fourth quartile of the list work well, but the second and third are progressively more off toward the center.
Just looking for some help on how to use positioning in WPF. All of the relativity and the difference between percentage-based coordinates and 'screen-unit' coordinates has me somewhat confused at this point.
After a lot of trial and error I ended up with this:
void ManipCompleted(object sender, ManipulationCompletedEventArgs e)
{
FocusTaker.Focus(); //reset focus
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < List.Items.Count; i++)
{
SurfaceListBoxItem item = List.ItemContainerGenerator.ContainerFromIndex(i) as SurfaceListBoxItem;
if (ViewportHelper.IsInViewport(item))
{
FrameworkElement el = item as FrameworkElement;
visibleElements.Add(el);
}
}
Window window = Window.GetWindow(this);
double center = ss.ViewportWidth / 2;
double closestCenterOffset = double.MaxValue;
FrameworkElement centerItem = visibleElements[0];
foreach (FrameworkElement el in visibleElements)
{
double centerOffset = Math.Abs(el.TransformToAncestor(window).Transform(new Point(0, 0)).X + (el.ActualWidth / 2) - center);
if (centerOffset < closestCenterOffset)
{
closestCenterOffset = centerOffset;
centerItem = el;
}
}
Point itemPosition = centerItem.TransformToAncestor(window).Transform(new Point(0, 0));
double desiredOffset = ss.HorizontalOffset - (center - itemPosition.X) + (centerItem.ActualWidth / 2);
ss.ScrollToHorizontalOffset(desiredOffset);
centerItem.Focus();
}
This block of code effectively determines which visible list element is overlapping the center line of the list and snaps that element to the exact center position. The snapping is a little abrupt, so I'll have to look into some kind of animation, but otherwise I'm fairly happy with it! I'll probably use something from here for animations: http://blogs.msdn.com/b/delay/archive/2009/08/04/scrolling-so-smooth-like-the-butter-on-a-muffin-how-to-animate-the-horizontal-verticaloffset-properties-of-a-scrollviewer.aspx
Edit: Well that didn't take long. I expanded the ScrollViewerOffsetMediator to include HorizontalOffset and then simply created the animation as suggested in the above post. Works like a charm. Hope this helps someone eventually.
Edit2: Here's the full code for SnapList:
SnapList.xaml
SnapList.xaml.cs
Note that I got pretty lazy as this project went on an hard-coded some of it. Some discretion will be needed to determine what you do and don't want from this code. Still, I think this should work pretty well as a starting point for anyone who wants this functionality.
The code has also changed from what I pasted above; I found that using Windows.GetWindow gave bad results when the list was housed in a control that could move. I made it so you can assign a control for your movement to be relative to (recommended that be the control just above your list in the hierarchy). I think a few other things changed as well; I've added a lot of customization options including being able to define a custom focal point for the list.
I've got a custom (and getting complex) TabControl. It's a gathering of many sources, plus my own wanted features. In it is a custom Panel to show the headers of the TabControl. Its features are to compress the size of the TabItems until they reached their minimum, and then activates scrolling features (in the Panel, again). There is also another custom panel to hold a single button, that renders on the right of the TabItems (it's a "new tab" button).
It all works great, until I try to animate the scrolling.
Here are some relevant snippets :
In the CustomTabPanel (C#, overriding Panel and implementing IScrollInfo):
private readonly TranslateTransform _translateTransform = new TranslateTransform();
public void LineLeft()
{
FirstVisibleIndex++;
var offset = HorizontalOffset + _childRects[0].Width;
if (offset < 0 || _viewPort.Width >= _extent.Width)
offset = 0;
else
{
if (offset + _viewPort.Width > _extent.Width)
offset = _extent.Width - _viewPort.Width;
}
_offset.X = offset;
if (_scrollOwner != null)
_scrollOwner.InvalidateScrollInfo();
//Animate the new offset
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset,
new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd) { AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
aScrollAnimation.Completed += ScrollAnimationCompleted;
_translateTransform.BeginAnimation(TranslateTransform.XProperty, aScrollAnimation , HandoffBehavior.SnapshotAndReplace);
//End of animation
// These lines are the only ones needed if we remove the animation
//_translateTransform.X = -offset;
//InvalidateMeasure();
}
void ScrollAnimationCompleted(object sender, EventArgs e)
{
InvalidateMeasure();
}
the _translateTransform is initialized in the constructor :
base.RenderTransform = _translateTransform;
Again, everything is fine if I remove the animation part and just replace it with the commented out lines at the end.
I must also point out that the problem is NOT with the animation itself. That part works out well. The problem is about when I remove some tab items : all the layout then screws up. The TranslateTransformation seems to hold on some wrong value, or something.
Thanks in advance.
Well. As it's often the case, I kept working on the thing, and... answered myself.
Could still be useful for other people, so here was the catch. In the line :
var aScrollAnimation = new DoubleAnimation(_translateTransform.X, -offset, new Duration(this.AnimationTimeSpan), FillBehavior.HoldEnd)
{ AccelerationRatio = 0.5, DecelerationRatio = 0.5 };
the FillBehavior should have been FillBehavior.Stop.
As easy as that!