I found a VirtualizingWrapPanel(VWP) project here which, I thought, could help me with optimizing my ListView scrolling performance. The listView have to has four columns and multiple rows to display the source items. So I tried to use this VWP, but scrolling(I made it as DoubleAnimation) smoothness still sucks, the framerate drops down to about 30 fps and it's very noticeable. I copied the source code from the link above to try to debug it and understand what is the reason of such a bad performance.
After several hours a found out that ArrangeOverride method of VWP and MeasureOverride method of its "inheritance parent" can execute about 20-30ms (1000ms / 30ms = ~30fps) and that's it, I found the reason why it's so slow... but why?
MeasureOverride method calls two potentially heavy methods: RealizeItems and VirtualizeItems
protected virtual void RealizeItems()
{
var startPosition = ItemContainerGenerator.GeneratorPositionFromIndex(ItemRange.StartIndex);
int childIndex = startPosition.Offset == 0 ? startPosition.Index : startPosition.Index + 1;
using (ItemContainerGenerator.StartAt(startPosition, GeneratorDirection.Forward, true))
{
for (int i = ItemRange.StartIndex; i <= ItemRange.EndIndex; i++, childIndex++)
{
UIElement child = (UIElement)ItemContainerGenerator.GenerateNext(out bool isNewlyRealized);
if (isNewlyRealized || /*recycled*/!InternalChildren.Contains(child))
{
if (childIndex >= InternalChildren.Count)
{
AddInternalChild(child);
}
else
{
InsertInternalChild(childIndex, child);
}
ItemContainerGenerator.PrepareItemContainer(child);
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
}
if (child is IHierarchicalVirtualizationAndScrollInfo groupItem)
{
groupItem.Constraints = new HierarchicalVirtualizationConstraints(
new VirtualizationCacheLength(0),
VirtualizationCacheLengthUnit.Item,
new Rect(0, 0, ViewportWidth, ViewportHeight));
child.Measure(new Size(ViewportWidth, ViewportHeight));
}
}
}
}
protected virtual void VirtualizeItems()
{
for (int childIndex = InternalChildren.Count - 1; childIndex >= 0; childIndex--)
{
var generatorPosition = GetGeneratorPositionFromChildIndex(childIndex);
int itemIndex = ItemContainerGenerator.IndexFromGeneratorPosition(generatorPosition);
if (!ItemRange.Contains(itemIndex))
{
if (VirtualizationMode == VirtualizationMode.Recycling)
{
ItemContainerGenerator.Recycle(generatorPosition, 1);
}
else
{
ItemContainerGenerator.Remove(generatorPosition, 1);
}
RemoveInternalChildRange(childIndex, 1);
}
}
}
So I understand that developers could make a mistake in code, I also understand that there may not be a single thing that can be optimized, but I really cannot understand why it is so slow.. The loops iterates throught about 40-50 UIElement objects, every of which has two child objects(TextBlock and ImageSource) and that's not such a huge number of objects..
I want to use parallel tasks in these loops, but UIElement and others is not thread-safe and can only be accessed in UI Thread... or can?
How can I increase performance of these methods? Can there be implemented efficient multithreading?
Thanks!
I found this method, which calls InvalidateMeasure() every time vertical scroll offset changes. Because of DoubleAnimation I use to animate vertical scroll offset this is code really often called, but if there is no one thing to change measure and arrange methods executes for about 1-2ms in total, else - a lot slowlier.
public void SetVerticalOffset(double offset)
{
if (offset < 0 || Viewport.Height >= Extent.Height)
{
offset = 0;
}
else if (offset + Viewport.Height >= Extent.Height)
{
offset = Extent.Height - Viewport.Height;
}
Offset = new Point(Offset.X, offset);
ScrollOwner?.InvalidateScrollInfo();
InvalidateMeasure();
}
UPDATE: I had opened Profiler and found out, that a lot of time(7-10 and more ms) is taken by layout operations.
UPDATE 2: The code bellow offsets(arranges) all the visible items
protected override Size ArrangeOverride(Size finalSize)
{
double offsetX = GetX(Offset);
double offsetY = GetY(Offset);
/* When the items owner is a group item offset is handled by the parent panel. */
if (ItemsOwner is IHierarchicalVirtualizationAndScrollInfo groupItem)
{
offsetY = 0;
}
Size childSize = CalculateChildArrangeSize(finalSize);
CalculateSpacing(finalSize, out double innerSpacing, out double outerSpacing);
for (int childIndex = 0; childIndex < InternalChildren.Count; childIndex++)
{
UIElement child = InternalChildren[childIndex];
int itemIndex = GetItemIndexFromChildIndex(childIndex);
int columnIndex = itemIndex % itemsPerRowCount;
int rowIndex = itemIndex / itemsPerRowCount;
double x = outerSpacing + columnIndex * (GetWidth(childSize) + innerSpacing);
double y = rowIndex * GetHeight(childSize);
if (GetHeight(finalSize) == 0.0)
{
/* When the parent panel is grouping and a cached group item is not
* in the viewport it has no valid arrangement. That means that the
* height/width is 0. Therefore the items should not be visible so
* that they are not falsely displayed. */
child.Arrange(new Rect(0, 0, 0, 0));
}
else
{
child.Arrange(CreateRect(x - offsetX, y - offsetY, childSize.Width, childSize.Height));
}
}
return finalSize;
}
Related
I have a fluids simulation running in Unity, calculating a heightfield for the fluid. I then have a mesh/vertex grid for which I set the height. I'm now trying to make it threaded. I only update the fluids every several frames (this is also threaded and works perfectly so far.) I also only update the mesh when the simulation got updated.
I have the following (non-threaded) code that works as it should:
public override void ApplyVisuals(float[][] lowerLayersHeight) {
UpdateVisuals(1, N, lowerLayersHeight, _height, _opaqueHeight, _nonZeroHeightOffset, _tempVertices, _tempColors);
_mesh.vertices = _tempVertices;
_mesh.colors32 = _tempColors;
}
So I update the visuals with some parameters, and then store the new vertices and new colors for the mesh in _tempVertices en _tempColors respectively. Though I tried to run this code on a separate thread:
public override void ApplyVisuals(float[][] lowerLayersHeight) {
if (_mainVisualsThread != null && _mainVisualsThread.IsAlive) {
_mainVisualsThread.Join();
}
Helpers.Swap(ref _vertices, ref _tempVertices); //swap the onces calculated by the _mainVisualsThread (temp) with the current array
Helpers.Swap(ref _colors, ref _tempColors);
_mesh.vertices = _tempVertices; //set the current as visual, the temps will be updated with the new vertices for next update
_mesh.colors32 = _tempColors;
_mainVisualsThread = new Thread(()=>
UpdateVisuals(1, N, lowerLayersHeight, _height, _opaqueHeight, _nonZeroHeightOffset, _tempVertices, _tempColors)
);
_mainVisualsThread.Start();
}
So instead of updating the visuals this call, I let a separate thread calculate the visuals for the next time ApplyVisuals and apply the vertices and colors calculated by the previous call to ApplyVisuals. This of course makes the visuals lag behind one update, but that's an acceptable trade-of. Though for some reason this doesn't work. My mesh starts to flicker, one frame being the proper one, the next nothing. The picture shows the bad frame first, and then the good one.
The weirdness doesn't stop there however. In my first non-threaded solution, the simulation works perfectly. Though when I use threads the visualisation somehow fucks it up. Even though I don't alter any variables used in the simulation. The only thing I change is my buffered vertex and colors array, but those aren't used in the simulation.
The UpdateVisuals method I use:
static void UpdateVisuals(int xFrom, int xTo,
float[][] lowerLayersHeight,
float[][] heightField,
float opaqueHeight,
float nonZeroHeightOffset,
Vector3[] vertices, Color32[] colors
) {
// Set the heights of the vertices of the mesh and apply colors
int x, y, index;
float height, relHeight;
for (x = xFrom; x <= xTo; ++x) {
for (y = 0; y < N+2; ++y) {
index = CalculateIndex(x,y);
height = heightField[x][y];
relHeight = height / opaqueHeight;
vertices[index].y = height + lowerLayersHeight[x][y]
+ (height > 0 ? nonZeroHeightOffset : 0);
colors[index].a = (byte)Mathf.Lerp(0, 200, relHeight);
}
}
}
In my simulation I never alter the _height array, what I do is I alter a _tempHeight array, and then swap them when the update is done. I do however read from the _height array.
Some more things I tried:
Another ApplyVisuals method:
_mainVisualsThread = new Thread(()=>
UpdateVisuals(1, N, lowerLayersHeight, _height, _opaqueHeight, _nonZeroHeightOffset, _tempVertices, _tempColors)
);
_mainVisualsThread.Start();
_mainVisualsThread.Join();
_mesh.vertices = _tempVertices;
_mesh.colors32 = _tempColors;
This also works as it should. So there appears to go something wrong when actually doing other stuff while the visualsThread is working.
I also tried to copy both the _height and lowerLayersHeight with the copy from this question: link Like this:
var bufferedHeight = Helpers.CopyArrayBuiltIn(_height);
var bufferedLowerLayersHeight = Helpers.CopyArrayBuiltIn(lowerLayersHeight);
_mainVisualsThread = new Thread(()=>
UpdateVisuals(1, N, bufferedLowerLayersHeight, bufferedHeight, _opaqueHeight, _nonZeroHeightOffset, _tempVertices, _tempColors)
);
This didn't help, and both the simulation and visualisation go wrong again :(
And then I was out of ideas. I'm pretty new to threading, so I'm hoping there's somthing simple I'm missing. But I can't figure out what.
Here's my simulation update code:
public override void DoUpdate(float dt, float dx, float[][] lowerLayersHeight) {
// Wait for update to be done
if (_mainUpdateThread != null && _mainUpdateThread.IsAlive) {
_mainUpdateThread.Join();
}
Helpers.Swap(ref _tempHeight, ref _height);
// Start the next update already on different threads. These will then be swapped next time an update is wanted.
_mainUpdateThread = new Thread(()=> {
// Height
RunPartsThreaded(_threadCount, (int from, int to)=>UpdateHeight(from, to, dt, dx, _tempFlux, _height, _tempHeight));
});
_mainUpdateThread.Start();
}
static void UpdateHeight(int xFrom, int xTo, float dt, float dx,
OutflowFlux[][] tempFlux,
float[][] height,
float[][] tempHeight
) {
int x, y;
float dV;
for (x=xFrom ; x <= xTo ; x++ ) {
for (y=1 ; y <= N ; y++ ) {
//
// 3.2.2 Water Surface (and Velocity Field Update)
// ----------------------------------------------------------------------------------------
dV = dt * (
//sum in
tempFlux[x-1][y].right + tempFlux[x][y-1].top + tempFlux[x+1][y].left + tempFlux[x][y+1].bottom
//minus sum out
- tempFlux[x][y].right - tempFlux[x][y].top - tempFlux[x][y].left - tempFlux[x][y].bottom
); //(6)
tempHeight[x][y] = height[x][y] + dV / (dx*dx); //(7)
//swap temp and the real one later
}
}
}
The code gets called every _dt seconds (usually 0.02-0.05) like this:
_timeSinceLastUpdate += Time.deltaTime;
if (_timeSinceLastUpdate >= _dt) {
_timeSinceLastUpdate -= _dt;
//
// Update each layer
// ----------------------------------------------------------------------
ResetTotalHeight(); //The first layer just sits on a plane
for (int i = 0; i < _layers.Count; ++i) {
_layers[i].DoUpdate(_dt, _dx, _tempTotalHeight);
_layers[i].ApplyVisuals(_tempTotalHeight);
AddHeightToTotal(_layers[i].HeightField);
}
}
There are multiple layers, though only one uses the fluid simulation code, the others are static. The AddHeightToTotal is just a method to keep track of the total height of all previous layers, that's the lowerLayersHeight parameter my simulation methods get.
Lastly, the RunPartsThreaded method I use:
static void RunPartsThreaded(int threadCount, Action<int, int> partUpdateWrapper) {
int extraThreadCount = threadCount - 1;
Thread[] _threads = new Thread[extraThreadCount];
int step = N / threadCount;
int i;
for (i = 0; i < extraThreadCount; ++i) {
int index = i; //https://stackoverflow.com/questions/7352757/threadstart-in-a-loop-in-a-timer-only-executing-last-thread-in-said-loop
_threads[index] = new Thread(()=>partUpdateWrapper(step * index + 1, step * (index+1)));
_threads[index].Start();
}
partUpdateWrapper( step * extraThreadCount + 1, N );
for (i = 0; i < extraThreadCount; ++i) {
_threads[i].Join();
}
}
The simulation can be run on only strips of my grid, and I make multiple threads each updating a different part of the whole grid.
Preamble
I'm not very optimistic about your multi-threading design overall... IMO, it would be nice at least to have threads reused.
Problem
Ok, take a look on this:
public override void DoUpdate(float dt, float dx, float[][] lowerLayersHeight) {
....
Helpers.Swap(ref _tempHeight, ref _height);
Ooops! UpdateVisuals thread can be runnning right now (and it's highly likely that it is), which uses _height array.
I have a dynamically created N panels, the distance between them in height 15px
panel.Location = new Point (x, y);
y = panel.Bottom + 15;
I can make the width of the smaller, and so I need distance in height between the panels was always 15px
I have a method with different checks for resize, and I try changes distance, but it always works differently...
public void checkResize(string msg_out, object panel_sender, object text_msg_sender, int panHei, int numbs)
{
Panel pan_item = (Panel)panel_sender;
Label lab_item = (Label)text_msg_sender;
char[] msg_arr = msg_out.ToCharArray();
int panWidRaz = 308 - pan_item.Width;
int panWidw = pan_item.Width;
if (int.Parse(pan_item.Name) != numbs - 1)
{
if (panWidw < buff)
{
/* if (panWidRaz % 15 == 0)
{
for (int i = int.Parse(pan_item.Name); i >= 0; i--)
{
panel1.Controls[i.ToString()].Location = new Point(panel1.Controls[i.ToString()].Location.X, panel1.Controls[i.ToString()].Location.Y + 1);
}
}*/
//width control becomes smaller panels are becoming more in height, it is necessary that the distance between the panels remained 15px
}
if (panWidw > buff)
{
/*if (panWidRaz % 15 == 0)
{
for (int i = int.Parse(pan_item.Name); i >= 0; i--)
{
panel1.Controls[i.ToString()].Location = new Point(panel1.Controls[i.ToString()].Location.X, panel1.Controls[i.ToString()].Location.Y - 1);
}
}*/
//width control becomes bigger panels are becoming less in height, it is necessary that the distance between the panels remained 15px
}
buffCountPan++;
if (buffCountPan == panel1.Controls.Count - 1)
{
buff = panWidw;
buffCountPan = 0;
}
if (msg_arr.Length > 26)
{
int panWid = (308 - pan_item.Width) / 5;
int panWidLab = 308 - pan_item.Width;
pan_item.Height = panHei + panWid;
lab_item.MaximumSize = new System.Drawing.Size(300 - panWidLab, 100);
lab_item.MinimumSize = new System.Drawing.Size(300 - panWidLab, 14);
}
}
}
I can't post image here... reputation... i make scrin of work my panel
http://pixs.ru/showimage/Bezimeni1p_9639414_8969341.png
If you want to make it simple, when you add those panel which i guess is done dynamically. You can set the panel.tag = "[Order]". so an order number assign so you know which one is first on top and the second and go on...
Const int MinimumStartLocation = 0;
Const int DistanceBetweenPanel = 15;
private void setPanelLocation(Panel pnl)
{
// retreive the order of this panel
int iPanelOrder = Convert.ToInt32(pnl.Tag);
// the first panel must show on top and have specific location
if(iPanelOrder == 0)
{
pnl.Top = MinimumStartLocation;
}
else
{
// set the top of the panel to the bottom value of the panel just before him in the order plus the constant
pnl.Top = this.Controls.OfType<Panel>().ToList().Find(pan => Convert.ToInt32(pan.Tag) == iPanelOrder -1).Bottom + DistanceBetweenPanel;
}
}
now use this LINQ command it will call the method above for each panel in the proper order.
this.Controls.OfType<Panel>().OrderBy(x => Convert.ToInt32(x.Tag)).ToList().ForEach(pan => setPanelLocation(pan));
here a sample project quickly done download here
Edit: updated link
It might be worth taking a look at the Flow Layout Panel. When you add a control to a flow layout panel it will automatically be positioned either to the left, right, top or bottom of any existing controls within that flow layout panel (depending on the layout direction property that you specify).
Similarly, if you remove or resize a control contained within a flow layout panel the positions of the controls will be automatically updated for you.
In your case it should be as simple as adding a new flow layout panel to your form then, for each panel that you add to the flow layout panel, setting the top margin to 15px and the bottom margin to 0px; the flow layout panel will take care of the rest. When panels are added, removed or resized the flow layout panel will ensure that they are still displayed correctly with a margin of 15px between them.
public void DrawingPulseData(byte[] data)
{
// Make sure that the curvelist has at least one curve
if (PulseControl.GraphPane.CurveList.Count <= 0)
return;
// Get the first CurveItem in the graph
LineItem curve = PulseControl.GraphPane.CurveList[0] as LineItem;
if (curve == null)
return;
// Get the PointPairList
IPointListEdit list = curve.Points as IPointListEdit;
// If this is null, it means the reference at curve.Points does not
// support IPointListEdit, so we won't be able to modify it
if (list == null)
return;
double time = (Environment.TickCount - tickStart) / 1000.0;
for (int i = 0; i < count; i++)
{
list.Add(time, (double)data[i]);
}
Scale xScale = PulseControl.GraphPane.XAxis.Scale;
if (time > xScale.Max - xScale.MajorStep)
{
xScale.Max = time + xScale.MajorStep;
xScale.Min = xScale.Max - 30.0;
}
// Make sure the Y axis is rescaled to accommodate actual data
PulseControl.AxisChange();
// Force a redraw
PulseControl.Invalidate();
count = 0;
}
Hi. I am using this method to draw real time data in zedgraph. count is length of incoming serial port data. This code works fine in timer(20ms) and draws data at each tick. However if i move this method into a class it does not work correctly. It draws too fast and wrong data.
public static void DrawingPulseData(byte[] data,ZedGraphControl zgc,int count, int TickStart)
I changed parameters like this after moving it into class. I changed PulseControl as zgc, tickstart as TickStart. I could not understand why it is not work same as the first code.
At the first picture,using code provided by #discomurray, i wrote this code statement out of the if's scopes. It gives me data like this.
Scale xScale = zgc.GraphPane.XAxis.Scale;
xScale.Max = now;
xScale.Min = now - 30.0;
If i write the same code statement into if's scopes data looks like picture above. It is a 10 seconds record. I don't have such a data with my method.
I assume that tickCount is the start time of the data buffer.
When adding the data to the list you need to change the x value (time) for each point in the list.
public static void DrawPulseData(byte[] data, ZedGraphControl zgc, int count, int tickStart)
{
double now = Environment.TickCount / 1000.0;
if (count != 0)
{
double span = (now - tickStart);
double inverseRate = span / count;
for (int i = 0; i < count; i++)
{
list.add(tickStart + ((i+1) * inverseRate), data[i]);
}
}
Scale xScale = zgc.GraphPane.XAxis.Scale;
xScale.Max = now;
xScale.Min = now - 30.0;
PulseControl.AxisChange();
PulseControl.Invalidate();
}
as for drawing to fast, it will only go as fast as you tell it go.
So anyone out there knows of sample code or control that perfectly emulates the Windows 8 Start Menu Tile Layout Engine?
It should support mixed Square and Rectangle Tiles, and repacks the square tiles over or under rectangle tiles properly.
Note: WrapPanel works if ALL TILES are Square. But once you mix in tiles that span 2-Squares worth of space, the layout breaks, and is inconsistent with the Windows 8 Start Menu
I am expecting code that extends the WPF Panel.
Disclaimer: Yes I have searched the Internet, the closest thing I've found is the CodeProject example, but that only works if all tiles are same-sized squares.
I've looked around myself and couldn't find anything to do what I/we want. I knew that to get this behavior we'd need some sort of custom panel object, so I set about creating one...
What it boils down to, is the tiles need to be arranged vertically, with double-width tiles taking up a whole row in that column, and normal width tiles to pair up. When it reaches the bottom of the container, it needs to create a new column and follow the same pattern.
Here's my implementation:
public class MetroTilePanel : Panel
{
protected override Size ArrangeOverride(System.Windows.Size finalSize)
{
double x = 0, y = 0, colWidth = 0, rowHeight = 0;
int col = 0;
colWidth = Children.Cast<UIElement>().Select(c => c.DesiredSize.Width).Max();
foreach (UIElement child in Children)
{
rowHeight = Math.Max(rowHeight, child.DesiredSize.Height);
if (x + child.DesiredSize.Width > (colWidth * (col + 1)))
{
// New row
y += rowHeight;
x = (colWidth * (col));
rowHeight = child.DesiredSize.Height;
}
if (y + rowHeight > finalSize.Height)
{
// New column
col++;
x = (colWidth * (col));
y = 0;
}
child.Arrange(new Rect(x, y, child.DesiredSize.Width, child.DesiredSize.Height));
x += child.DesiredSize.Width;
}
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
double x = 0, y = 0, colWidth = 0;
foreach (UIElement child in Children)
{
child.Measure(availableSize);
if (x + child.DesiredSize.Height > availableSize.Height)
{
x += colWidth;
y = 0;
colWidth = 0;
}
y += child.DesiredSize.Height;
if (child.DesiredSize.Width > colWidth)
{
colWidth = child.DesiredSize.Width;
}
}
x += colWidth;
var resultSize = new Size();
resultSize.Width = double.IsPositiveInfinity(availableSize.Width) ? x : availableSize.Width;
resultSize.Height = double.IsPositiveInfinity(availableSize.Height) ? y : availableSize.Height;
return resultSize;
}
}
Screenshot of the control in action:
Disclaimers:
The MeasureOverride only works by chance, and isn't setup correctly.
If you want the nice MetroTile layout, then stick to uniform sizes i.e 100x100 and 200x100
I haven't fully tested it, but I will be implementing it into my fake-Metro app, so if you want to see any future changes, just holler.
If you want the proper GridView tiling behavior, then we'd have to create a brand new control (to support dragging items around etc).
I hope this helps.
These are the two different libraries I evaluated for my project to create a Windows 8 like startpage in WPF:
Telerik RadTileList (paid)
mahapps.metro (open source)
I have a custom control that zooms on a custom drawn document canvas.
I tried using AutoScroll but it was not giving satisfactory results. When I would set AutoScrollPosition and AutoScrollMinSize back to back (in any order) it would force a paint and cause jitter each time the zoom changes. I assume this was because it was calling an Update and not Invalidate when I modified both properties.
I am now manually setting the HorizontalScroll and VerticalScroll properties with AutoScroll set to false like so each time the Zoom level or the client size changes:
int canvasWidth = (int)Math.Ceiling(Image.Width * Zoom) + PageMargins.Horizontal;
int canvasHeight = (int)Math.Ceiling(Image.Height * Zoom) + PageMargins.Vertical;
HorizontalScroll.Maximum = canvasWidth;
HorizontalScroll.LargeChange = ClientSize.Width;
VerticalScroll.Maximum = canvasHeight;
VerticalScroll.LargeChange = ClientSize.Height;
if (canvasWidth > ClientSize.Width)
{
HorizontalScroll.Visible = true;
}
else
{
HorizontalScroll.Visible = false;
HorizontalScroll.Value = 0;
}
if (canvasHeight > ClientSize.Height)
{
VerticalScroll.Visible = true;
}
else
{
VerticalScroll.Visible = false;
VerticalScroll.Value = 0;
}
int focusX = (int)Math.Floor((FocusPoint.X * Zoom) + PageMargins.Left);
int focusY = (int)Math.Floor((FocusPoint.Y * Zoom) + PageMargins.Top);
focusX = focusX - ClientSize.Width / 2;
focusY = focusY - ClientSize.Height / 2;
if (focusX < 0)
focusX = 0;
if (focusX > canvasWidth - ClientSize.Width)
focusX = canvasWidth - ClientSize.Width;
if (focusY < 0)
focusY = 0;
if (focusY > canvasHeight - ClientSize.Height)
focusY = canvasHeight - ClientSize.Height;
if (HorizontalScroll.Visible)
HorizontalScroll.Value = focusX;
if (VerticalScroll.Visible)
VerticalScroll.Value = focusY;
In this case, FocusPoint is a PointF structure that holds the coordinates in the bitmap which the user is focused on (for example, when they mouse wheel to zoom in they are focusing on the current mouse location at that time). This functionality works for the most part.
What does not work is the scroll bars. If the user tries to manually scroll by clicking on either scroll bar, they both keep returning to 0. I do not set them anywhere else in my code. I have tried writing the following in the OnScroll() method:
if (se.ScrollOrientation == ScrollOrientation.VerticalScroll)
{
VerticalScroll.Value = se.NewValue;
}
else
{
HorizontalScroll.Value = se.NewValue;
}
Invalidate();
But this causes some very erratic behavior including flicking and scrolling out of bounds.
How am I supposed to write the code for OnScroll? I've tried the base.OnScroll but it didn't do anything while AutoScroll is set to false.
I ended up implementing my own custom scrolling by creating 3 child controls: an HScrollBar, a VScrollBar, and a Panel.
I hide ClientSize and ClientRectangle like so:
public new Rectangle ClientRectangle
{
get
{
return new Rectangle(new Point(0, 0), ClientSize);
}
}
public new Size ClientSize
{
get
{
return new Size(
base.ClientSize.Width - VScrollBar.Width,
base.ClientSize.Height - HScrollBar.Height
);
}
}
The layout is done in OnClientSizeChanged:
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
HScrollBar.Location = new Point(0, base.ClientSize.Height - HScrollBar.Height);
HScrollBar.Width = base.ClientSize.Width - VScrollBar.Width;
VScrollBar.Location = new Point(base.ClientSize.Width - VScrollBar.Width, 0);
VScrollBar.Height = base.ClientSize.Height - HScrollBar.Height;
cornerPanel.Size = new Size(VScrollBar.Width, HScrollBar.Height);
cornerPanel.Location = new Point(base.ClientSize.Width - cornerPanel.Width, base.ClientSize.Height - cornerPanel.Height);
}
Each ScrollBar has their Scroll event subscribed to the following:
private void ScrollBar_Scroll(object sender, ScrollEventArgs e)
{
OnScroll(e);
}
And finally we can allow MouseWheel events to scroll with the following:
protected override void OnMouseWheel(MouseEventArgs e)
{
int xOldValue = VScrollBar.Value;
if (e.Delta > 0)
{
VScrollBar.Value = (int)Math.Max(VScrollBar.Value - (VScrollBar.SmallChange * e.Delta), 0);
OnScroll(new ScrollEventArgs(ScrollEventType.ThumbPosition, xOldValue, VScrollBar.Value, ScrollOrientation.VerticalScroll));
}
else
{
VScrollBar.Value = (int)Math.Min(VScrollBar.Value - (VScrollBar.SmallChange * e.Delta), VScrollBar.Maximum - (VScrollBar.LargeChange - 1));
OnScroll(new ScrollEventArgs(ScrollEventType.ThumbPosition, xOldValue, VScrollBar.Value, ScrollOrientation.VerticalScroll));
}
}
For custom painting, you would use the following statement:
e.Graphics.TranslateTransform(-HScrollBar.Value, -VScrollBar.Value);
This worked perfectly without the glitches present when using AutoScroll.