I'm writing an image viewer control in C# inheriting from ScrollableControl. It allows zooming and panning the view on the image and has scroll bars reflecting the visible image area.
I'm using the integrated HorizontalScroll and VerticalScroll and could implement updating them to appear as expected (setting Visible, Maximum, LargeChange andValue most prominently):
SizeF viewSize = new SizeF(ClientSize.Width / Zoom, ClientSize.Height / Zoom);
SizeF freeTopLeft = new SizeF(-_offset.X, -_offset.Y);
SizeF freeBotRight = new SizeF(
viewSize.Width - ImageSize.Width - freeTopLeft.Width,
viewSize.Height - ImageSize.Height - freeTopLeft.Height);
// Horizontal
if (freeTopLeft.Width < 0 || freeBotRight.Width < 0)
{
HorizontalScroll.Visible = true;
HorizontalScroll.Maximum = (int)ImageSize.Width;
HorizontalScroll.LargeChange = (int)viewSize.Width;
HorizontalScroll.Value = (int)-freeTopLeft.Width;
}
else
{
HorizontalScroll.Visible = false;
}
// Vertical same as horizontal using Height, removed for brevity.
When the user drags the scrollbar thumb, I correctly update the displayed part of the image as follows in OnScroll(ScrollEventArgs se):
switch (se.ScrollOrientation)
{
case ScrollOrientation.HorizontalScroll:
_offset.X = se.NewValue;
break;
case ScrollOrientation.VerticalScroll:
_offset.Y = se.NewValue;
break;
}
Refresh();
However, as soon as the user releases the mouse button he dragged the thumb with, both scroll bars jump back to Value 0. It does not create another OnScroll event, so my image still shows the expected part, but the scroll bars "desynced" with what is displayed:
I checked many times if my code somehow manipulates the Value unexpectedly while and after scrolling, but I never set that property erroneously.
I also tried setting the ScrollBar values manually in OnScroll, but it did not help.
What do I need to do to correctly keep the thumb in place? What am I doing wrong?
Related
I'm adding buttons dynamically to a Form and am trying to lay the out side by side. I'm perfectly content with using the latest Button.Right as a starting point for the next button (with a margin left between of course), but the buttons have to adjust to fit the text.
So, what I'm doing is setting the AutoResize property to true and then storing the Right property, which however does not work because I guess the resizing doesn't happen until the button is drawn (I think). I tried Invalidate(), Refresh(), Update() and I think a couple more functions, and of course all together, but to no avail, I still get the old position and the next button starts beneath this one.
So the question is, after setting AutoResize to true on a Forms component, how do I force it to resize so I can grab the new Width/Right without waiting for the window to be redrawn?
Thanks in advance!
Note: If all else fails I'll do a rough approximation of the width of the buttons based on the string's length, so don't bother with something too fancy as a solution, it's not a requirement that it is perfect
You can use the Control's GetPreferredSize Method to obtain the final auto-sized dimensions. The Font property must either be explicitly set or the Control must be parented to a displayed control such that it can inherit the Font to use in the layout. In the following example, the control's Parent property is set so that it inherits the parent control's Font.
private Random rnd = new Random(1000);
private void button1_Click(object sender, EventArgs e)
{
const Int32 xDelta = 5; // the horizontal distance between the added Buttons
Int32 y = button1.Location.Y + 5 + button1.Height;
Int32 x = button1.Location.X;
Point loc = new Point(x, y);
this.SuspendLayout(); // this is Form that is the Parent container of the Buttons
for (Int32 i = 1; i <= 10; i++)
{
Button btn = new Button { Parent = this, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink };
btn.Text = new string('A', rnd.Next(1, 21));
btn.Location = loc;
Size sz = btn.GetPreferredSize(Size.Empty); // the size of btn based on Font and Text
loc.Offset(sz.Width + xDelta, 0);
}
this.ResumeLayout(true);
}
In my code I draw a rectangle and usually the rectangle is too large for the screen, even when maximised. I have set the form property AutoScroll to true and this doesn't seem to do anything. There won't be anything else on my form except the rectangle painting, how can I implement a vertical and horizontal scroll?
PrintingDesignForm form = new PrintingDesignForm();
form.Paint += (se, pe) => {
var r = new Rectangle(parameters.RectangleXPosition, parameters.RectangleYPosition, (int)Math.Ceiling(parameters.RectangleWidth) * 72, (int)Math.Ceiling(parameters.RectangleLength) * 72);
var brush = new SolidBrush(Color.FromArgb(255, 255, 204));
pe.Graphics.FillRectangle(brush, r);
using (var pen = new Pen(brush.Color, 2))
pe.Graphics.DrawRectangle(pen, r);
};
form.WindowState = FormWindowState.Maximized;
form.Show();
Setting AutoScroll = true on a Control/Form alone will only ensure that all Controls you add/nest to the parent will either show or can be reached by the scrollbars which will show up as needed.
This doesn't do anything for stuff you draw.
To make the drawing scrollable you need to set the AutoScrollMinSize to large enough values.
If you don't know in advance then at least while drawing you should be able to determine them from your data.
In Form Properties Set
AutoScroll = True
I'm working on a C# project using .NET 3.5 and Windows Forms. I need to design a decision step with multiple options that require a bit of explanatory text. For this, I want to have a set of RadioButtons to choose an option, followed by an additional Label each that contains the explanation.
I want to keep the label of the radio buttons and the label containing the explanatory text aligned - I've added red lines to the image to illustrate this. I could probably tweak some margins or other settings on the second label, but that would probably start to look weird as soon as the user chooses a different theme or changes some other settings. What is the canonical (and most robust) way to do this?
Your question boils down to two partial problems:
How large is the RadioButton (or the CheckBox when thinking ahead)..
How large is the gap between the glyph and the Text.
The first question is trivial:
Size s = RadioButtonRenderer.GetGlyphSize(graphics,
System.Windows.Forms.VisualStyles.RadioButtonState.CheckedNormal);
..using a suitable Graphics object. Note that I use the RadioButtonState CheckedNormal as I don't you want the Lables to align differently when the Buttons are checked or unchecked..
The second one is anything but trivial. The gap may or may not be constant and there is another gap to the left of the glyph! If I really wanted to get it right I guess I would write a routine to measure the text offset at startup:
public Form1()
{
InitializeComponent();
int gapRB = getXOffset(radioButton1);
int gapLB = getXOffset(label1);
label1.Left = radioButton1.Left + gapRB - gapLB;
}
Here is the measurement function. Note that is doesn't even use the Glyph measurement. Also note that it isn't enough to measure the text offset of the RadioButton. You also need to measure the offset of the Label!
int getXOffset(Control ctl)
{
int offset = -1;
string save = ctl.Text; Color saveC = ctl.ForeColor; Size saveSize = ctl.Size;
ContentAlignment saveCA = ContentAlignment.MiddleLeft;
if (ctl is Label)
{
saveCA = ((Label)ctl).TextAlign;
((Label)ctl).TextAlign = ContentAlignment.BottomLeft;
}
using (Bitmap bmp = new Bitmap(ctl.ClientSize.Width, ctl.ClientSize.Height))
using (Graphics G = ctl.CreateGraphics() )
{
ctl.Text = "_";
ctl.ForeColor = Color.Red;
ctl.DrawToBitmap(bmp, ctl.ClientRectangle);
int x = 0;
while (offset < 0 && x < bmp.Width - 1)
{
for (int y = bmp.Height-1; y > bmp.Height / 2; y--)
{
Color c = bmp.GetPixel(x, y);
if (c.R > 128 && c.G == 0) { offset = x; break; }
}
x++;
}
}
ctl.Text = save; ctl.ForeColor = saveC; ctl.Size = saveSize;
if (ctl is Label) { ((Label)ctl).TextAlign = saveCA; }
return offset;
}
Now the Texts do align pixel perfect..:
Note that I use two original controls from my form. Therefore much of the code is simply storing and restoring the properties I need to manipulate for the measurement; you can save a few lines by using two dummies.. Also note that I wrote the routine so that it can measure RadioButtons and Labels and probably CheckBoxes as well..
Is it worth it? You decide..!
PS: You could also owner-draw the RadioButton and the Label text in one.. this would have the interesting side-effect, that the whole text would be clickable..:
Here is a quick and dirty implementation of owner drawing a CheckBox: Prepare it by setting AutoSize = false and by adding the real text together with the extra text into the Tag, separated by a e.g. "§". Feel free to change this setup, maybe using the Label control..
I clear the Text to prevent it from drawing it and I decide on an offset. To measure it, you could use the GetGlyphSize from above.. Note how the DrawString method honors embedded '\n' characters.
The Tag contained this string:
A Rose is a Rose is a Rose..§A Rose is a rose is a rose is a rose is /
A rose is what Moses supposes his toes is / Couldn't be a lily or a
taffy daphi dilli / It's gotta be a rose cuz it rhymes with mose!
And I for the screenshot I actually used this line:
e.Graphics.DrawString(texts[1].Replace("/ ", "\n"), ...
Here is the Paint event:
private void checkBox1_Paint(object sender, PaintEventArgs e)
{
checkBox1.Text = "";
string[] texts = checkBox1.Tag.ToString().Split('§');
Font font1 = new Font(checkBox1.Font, FontStyle.Regular);
e.Graphics.DrawString(texts[0], checkBox1.Font, Brushes.Black, 25, 3);
if (texts.Length > 0)
{
SizeF s = e.Graphics.MeasureString(texts[1], checkBox1.Font, checkBox1.Width - 25);
checkBox1.Height = (int) s.Height + 30;
e.Graphics.DrawString(texts[1], font1, Brushes.Black,
new RectangleF(new PointF(25, 25), s));
}
}
The simplest out-of-the-box solution (it seems to me) would be to use 3 controls instead of 2: a radio button (with the text set to ""), a label (to go beside the radio button) and another label (to go below them). This would allow you easier configuration in designer, but (far more importantly) simpler run-time evaluation and adjustment, if necessary, to keep them in alignment should styles change.
I do understand that this takes away the benefit of clicking the label to select the radio button, but you could add that behavior in the label's Click event if you need it.
Alternatively, you could create a UserControl containing the text-free radio button and the label, and handle the behavior within that UserControl while exposing the label's location.
If you don't care about the radiobutton's text being bold, you could set it's label to a multiline string, and set CheckAlign to TopLeft:
radioButton2.CheckAlign = ContentAlignment.TopLeft;
radioButton2.Text = #"Radiobutton
Explanation text";
Don't know why I didn't think of this earlier, but the following approach seems to work:
Use a TableLayoutPanel with two columns that are set to adjust their width automatically.
Place all RadioButtons in the first column and set them to span both columns.
Place all Labels in the second column, setting all margins to 0.
Add a disabled, but visible (!) "spacer" RadioButton without text in an additional row at the end of the layout.
When displaying the form, convert the first column to a fixed size and hide the "spacer".
The key point seems to be that the "spacer" has to be visible initially - otherwise the column will get a size of 0.
This is my test form in the designer:
To change the layout, I used the following Load handler:
private void TestForm_Load(object sender, EventArgs e)
{
// find the column with the spacer and back up its width
int column = tableLayoutPanel.GetColumn(radioButtonSpacer);
int width = tableLayoutPanel.GetColumnWidths()[column];
// hide the spacer
radioButtonSpacer.Visible = false;
// set the column to the fixed width retrieved before
tableLayoutPanel.ColumnStyles[column].SizeType = SizeType.Absolute;
tableLayoutPanel.ColumnStyles[column].Width = width;
}
And this is the result at runtime:
You could add an invisible dummy label having the same text as the radiobutton. Then, get the length of that label and calculate the correct position of the explanation label.
labelDummy.Text = radioButton1.Text;
labelExplanation.Left = radioButton1.Right - labelDummy.Width;
However, this still appears to be some pixels off, even though I the label's margin to 0, maybe some additional tweaking can fix this. Here's a screenshot to show what I mean. The label's background is green to be able to see the extra margin.
I'm currently trying to create a little plot interactive editor, using WPF.
On maximized window the plot dragging with mouse is not responsive enough because of the plot grid.
I got a path for my plot grid lying inside a Canvas control (render transform just shifts it to the bottom of the canvas)
<Path Name="VisualGrid" RenderTransform="{StaticResource PlotTechnicalAdjust}" Style="{DynamicResource ResourceKey=GridStyle}" Panel.ZIndex="1"/>
Here is how grid is created; _curState has actual camera "viewport" metadata
if (_curState.Changes.ScaleStepXChanged)
{
foreach (TextBlock item in _xLabels)
{
DeleteLabel(item);
}
_xLabels.Clear();
double i = _curState.LeftEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.VerticalLines] = new GeometryGroup { Transform = _verticalLinesShift};
var verticalLines =(GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.VerticalLines];
while (i <= _curState.RightEdgeLine * (1.001))
{
verticalLines.Children.Add(new LineGeometry(new Point(i * _plotParameters.PixelsPerOneX, 0),
new Point(i * _plotParameters.PixelsPerOneX,
-_wnd.ContainerGeneral.Height)));
_xLabels.Add(CreateLabel(i, Axis.X));
i += _curState.CurrentScaleStepX;
}
_curState.Changes.ScaleStepXChanged = false;
}
if (_curState.Changes.ScaleStepYChanged)
{
foreach (TextBlock item in _yLabels)
{
DeleteLabel(item);
}
_yLabels.Clear();
double i = _curState.BottomEdgeLine;
_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines] = new GeometryGroup { Transform = _horizontalLinesShift};
var horizontalLines = (GeometryGroup)_gridGeom.Children[(int)GridGeomIndexes.HorizontalLines];
while (i <= _curState.TopEdgeLine * (1.001))
{
horizontalLines.Children.Add(new LineGeometry(new Point(0, -i * _plotParameters.PixelsPerOneY),
new Point(_wnd.ContainerGeneral.Width,
-i * _plotParameters.PixelsPerOneY)));
_yLabels.Add(CreateLabel(i, Axis.Y));
i += _curState.CurrentScaleStepY;
}
_curState.Changes.ScaleStepYChanged = false;
}
Where Transforms are composition of TranslateTransform and ScaleTransform (for vertical lines I only use X components and only Y for horizontal lines).
After beeing created those GeometryGroups are only edited if a new line apears into camera or an existing line exits viewable space. Grid is only recreated when axis graduations have to be changed after zooming.
I have a dragging option implemented like this:
private Point _cursorOldPos = new Point();
private void OnDragPlotMouseMove(object sender, MouseEventArgs e)
{
if (e.Handled)
return;
Point cursorNewPos = e.GetPosition(ContainerGeneral);
_plotView.TranslateShiftX.X += cursorNewPos.X - _cursorOldPos.X;
_plotView.TranslateShiftY.Y += cursorNewPos.Y - _cursorOldPos.Y;
_cursorOldPos = cursorNewPos;
e.Handled = true;
}
This works perfectly smooth with a small window (1200x400 units) for a large amount of points (like 100+).
But for a large window (fullscreen 1920x1080) it happens pretty jittery even without any data-point controls on canvas.
The strange moment is that lags don't appear when I order my GridGenerator to keep around 100+ lines for small window and drag performance suffers when I got less than 50 lines on maximezed. It makes me think that it might somehow depend not on a number of elements inside a geometry, but on their linear size.
I suppose I should mention that OnSizeChanged I adjust the ContainerGeneral canvas' height and width and simply re-create the grid.
Checked the number of lines stored in runtime to make sure I don't have any extras. Tried using Image with DrawingVisual instead of Path. Nothing helped.
Appearances for clearer understanding
It was all about stroke dashes and WPF's unhealthy desire to count them all while getting hit test bounds for DrawingContext.
The related topic is Why does use of pens with dash patterns cause huge (!) performance degredation in WPF custom 2D drawing?
I am using a flowlayoutpanel which have a lot of buttons per logic sake. I'm having an issue of when I resize the window, I'm not I'm not able to see all the buttons lined up horizontally when the window gets smaller. Instead as the window gets smaller, the buttons drops down to the next line. Can anyone help me on how to resolve this issue? I just want the buttons to line up horizontally, when the window gets smaller, have a horizontal scrollbar. Below is what I have.
fLayoutPnl.Controls.Add(btn1);
// snipped adding buttons from 2 to 15
fLayoutPnl.Controls.Add(btn16);
fLayoutPnl.Dock = System.Windows.Forms.DockStyle.Top;
fLayoutPnl.Location = new System.Drawing.Point(0, 10);
fLayoutPnl.Name = "fLayoutPnl";
fLayoutPnl.Size = new System.Drawing.Size(1245, 30);
If you dock the flowlayoutpanel on the top, it take the size of the parent control.
So if you want a horizontal scroll, you need to set the AutoScrollMinSize of the form (or usercontrol).
Otherwise, you can do this :
this.AutoScroll = true;
this.fLayoutPnl.Dock = System.Windows.Forms.DockStyle.None;
this.fLayoutPnl.AutoSize = true;
this.fLayoutPnl.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.fLayoutPnl.Location = new System.Drawing.Point(0, 10);
this.fLayoutPnl.Name = "fLayoutPnl";
this.fLayoutPnl.Size = new System.Drawing.Size(1245, 30);
fLayoutPnl.WrapContents = false;
This would solve the issue. If a scroll bar is needed, set the MinimumSize property of the panel, after which the scroll bar should appear
To view all the contents of flow layout panel by scrolling vertically, set AutoScroll property to True and don't forget to set WrapContents property to True.
If the contents are to be viewed by scrolling horizontally, set AutoScroll property to True and don't forget to set WrapContents property to False.