Related
I am trying to scale all controls in my program (the ones in the tabcontrol too) when user resizes the window.
I know how to resize the controls, but i wanted my program to have everything scaled, so the ui i easier to read
i am not a good explainer, so here's a reference picture:
I have seen a lot of discussions about resizing the controls, but they are not very helpful.
can i achieve this effect in winforms?
One way to scale winforms controls with window size is to use nested TableLayoutPanel controls and set the rows and columns to use percent rather than absolute sizing.
Then, place your controls in the cells and anchor them on all four sides. For buttons that have a background image set to BackgroundImageLayout = Stretch this is all you need to do. However, controls that use text may require a custom means to scale the font. For example, you're using ComboBox controls where size is a function of the font not the other way around. To compensate for this, these actual screenshots utilize an extension to do a binary search that changes the font size until a target control height is reached.
The basic idea is to respond to TableLayoutPanel size changes by setting a watchdog timer, and when the timer expires iterate the control tree to apply the BinarySearchFontSize extension. You may want to clone the code I used to test this answer and experiment for yourself to see how the pieces fit together.
Something like this would work but you'll probably want to do more testing than I did.
static class Extensions
{
public static float BinarySearchFontSize(this Control control, float containerHeight)
{
float
vertical = BinarySearchVerticalFontSize(control, containerHeight),
horizontal = BinarySearchHorizontalFontSize(control);
return Math.Min(vertical, horizontal);
}
/// <summary>
/// Get a font size that produces control size between 90-100% of available height.
/// </summary>
private static float BinarySearchVerticalFontSize(Control control, float containerHeight)
{
var name = control.Name;
switch (name)
{
case "comboBox1":
Debug.WriteLine($"{control.Name}: CONTAINER HEIGHT {containerHeight}");
break;
}
var proto = new TextBox
{
Text = "|", // Vertical height independent of text length.
Font = control.Font
};
using (var graphics = proto.CreateGraphics())
{
float
targetMin = 0.9F * containerHeight,
targetMax = containerHeight,
min, max;
min = 0; max = proto.Font.Size * 2;
for (int i = 0; i < 10; i++)
{
if(proto.Height < targetMin)
{
// Needs to be bigger
min = proto.Font.Size;
}
else if (proto.Height > targetMax)
{
// Needs to be smaller
max = proto.Font.Size;
}
else
{
break;
}
var newSizeF = (min + max) / 2;
proto.Font = new Font(control.Font.FontFamily, newSizeF);
proto.Invalidate();
}
return proto.Font.Size;
}
}
/// <summary>
/// Get a font size that fits text into available width.
/// </summary>
private static float BinarySearchHorizontalFontSize(Control control)
{
var name = control.Name;
// Fine-tuning
string text;
if (control is ButtonBase)
{
text = "SETTINGS"; // representative max staing
}
else
{
text = string.IsNullOrWhiteSpace(control.Text) ? "LOCAL FOLDERS" : control.Text;
}
var protoFont = control.Font;
using(var g = control.CreateGraphics())
{
using (var graphics = control.CreateGraphics())
{
int width =
(control is ComboBox) ?
control.Width - SystemInformation.VerticalScrollBarWidth :
control.Width;
float
targetMin = 0.9F * width,
targetMax = width,
min, max;
min = 0; max = protoFont.Size * 2;
for (int i = 0; i < 10; i++)
{
var sizeF = g.MeasureString(text, protoFont);
if (sizeF.Width < targetMin)
{
// Needs to be bigger
min = protoFont.Size;
}
else if (sizeF.Width > targetMax)
{
// Needs to be smaller
max = protoFont.Size;
}
else
{
break;
}
var newSizeF = (min + max) / 2;
protoFont = new Font(control.Font.FontFamily, newSizeF);
}
}
}
return protoFont.Size;
}
}
Edit
The essence of my answer is use nested TableLayoutPanel controls and I didn't want to take away from that so I had given a reference link to browse the full code. Since TableLayoutPanel is not a comprehensive solution without being able to scale the height of ComboBox and other Fonts (which isn't all that trivial for the example shown in the original post) my answer also showed one way to achieve that. In response to the comment below (I do want to provide "enough" info!) here is an appendix showing the MainForm code that calls the extension.
Example:
In the method that loads the main form:
Iterate the control tree to find all the TableLayoutPanels.
Attach event handler for the SizeChanged event.
Handle the event by restarting a watchdog timer.
When the timer expires, _iterate the control tree of each TableLayoutPanel to apply the onAnyCellPaint method to obtain the cell metrics and call the extension.
By taking this approach, cells and controls can freely be added and/or removed without having to change the scaling engine.
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if(!DesignMode)
{
comboBox1.SelectedIndex = 0;
IterateControlTree(this, (control) =>
{
if (control is TableLayoutPanel tableLayoutPanel)
{
tableLayoutPanel.SizeChanged += (sender, e) => _wdtSizeChanged.StartOrRestart();
}
});
_wdtSizeChanged.PropertyChanged += (sender, e) =>
{
if (e.PropertyName!.Equals(nameof(WDT.Busy)) && !_wdtSizeChanged.Busy)
{
IterateControlTree(this, (control) =>
{
if (control is TableLayoutPanel tableLayoutPanel)
{
IterateControlTree(tableLayoutPanel, (child) => onAnyCellPaint(tableLayoutPanel, child));
}
});
}
};
}
// Induce a size change to initialize the font resizer.
BeginInvoke(()=> Size = new Size(Width + 1, Height));
BeginInvoke(()=> Size = new Size(Width - 1, Height));
}
// Browse full code sample to see WDT class
WDT _wdtSizeChanged = new WDT { Interval = TimeSpan.FromMilliseconds(100) };
SemaphoreSlim _sslimResizing= new SemaphoreSlim(1);
private void onAnyCellPaint(TableLayoutPanel tableLayoutPanel, Control control)
{
if (!DesignMode)
{
if (_sslimResizing.Wait(0))
{
try
{
var totalVerticalSpace =
control.Margin.Top + control.Margin.Bottom +
// I'm surprised that the Margin property
// makes a difference here but it does!
tableLayoutPanel.Margin.Top + tableLayoutPanel.Margin.Bottom +
tableLayoutPanel.Padding.Top + tableLayoutPanel.Padding.Bottom;
var pos = tableLayoutPanel.GetPositionFromControl(control);
int height;
float optimal;
if (control is ComboBox comboBox)
{
height = tableLayoutPanel.GetRowHeights()[pos.Row] - totalVerticalSpace;
comboBox.DrawMode = DrawMode.OwnerDrawFixed;
optimal = comboBox.BinarySearchFontSize(height);
comboBox.Font = new Font(comboBox.Font.FontFamily, optimal);
comboBox.ItemHeight = height;
}
else if((control is TextBoxBase) || (control is ButtonBase))
{
height = tableLayoutPanel.GetRowHeights()[pos.Row] - totalVerticalSpace;
optimal = control.BinarySearchFontSize(height);
control.Font = new Font(control.Font.FontFamily, optimal);
}
}
finally
{
_sslimResizing.Release();
}
}
}
}
internal void IterateControlTree(Control control, Action<Control> fx)
{
if (control == null)
{
control = this;
}
fx(control);
foreach (Control child in control.Controls)
{
IterateControlTree(child, fx);
}
}
}
Sorry this is so long, I wanted to proved enough code as it's hard to explain on it's own
I'm setting up a small test program to make a 2D visual gauge. The structure of the project is that I have a class; 'DirectX11Renderer' which initialises all the SharpDX objects to set up the factories, devices, back buffers, rendertargets, swapchains and layers.
This class then contains drawing methods for drawing basic shapes, filled shapes and text, they can likely be ignored, however the idea of the program is to be able to render the static content of the gauge on one layer (the background, stuff that doesn't really move) and the dynamic content on another layer, so each drawing method has an enum as an argument which specifies the layer to draw on, and then that is passed into this method, which starts drawing on the render target (if not already), pops the last pushed layer (if there was one) and pushes the layer that is required to be drawn on:
private void StartDrawingAndSwitchLayer(TargetLayer layer, bool draw = true)
{
if (draw && !isDrawing)
{
d2dRenderTarget.BeginDraw();
isDrawing = true;
}
switch (layer)
{
case TargetLayer.Static:
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
if (!isStaticLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, staticLayer);
isStaticLayerPushed = true;
}
break;
case TargetLayer.Dynamic:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (!isDynamicLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, dynamicLayer);
isDynamicLayerPushed = true;
}
break;
case TargetLayer.None:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
break;
}
if (!draw && isDrawing)
{
d2dRenderTarget.EndDraw();
isDrawing = false;
}
}
So in a class that uses an instance of this DirectX11Renderer class, it starts by setting the background colour without pushing a layer, rendering the static content by clearing the layer then calling several of the draw methods using the static layer as an argument for the enum to push the static layer, then rendering the dynamic content every time a value changes, doing the same thing by using the enum as an argument to pop the static layer and push the dynamic layer, then drawing on it. Finally at the end of the dynamic drawing method I present the frame by using the enum argument of TargetLayer.None to pop all layers, and then present the frame.
What I'm finding is that every time I change the value, (rendering the dynamic content on it's layer, popping the layer then presenting) the screen alternates between showing the content of the dynamic layer on a black background, and all the static and dynamic content as I would want to see it if this were working 100%.
As a minimum working example, here is the code for a form which contains the gauge control:
using System.Windows.Forms;
using System;
namespace SharpDXGauge
{
public partial class Form1 : Form
{
Gauge control;
public Form1()
{
InitializeComponent();
control = new Gauge(100, 0);
control.Dock = DockStyle.Left;
this.Controls.Add(control);
}
private void numericUpDown1_ValueChanged(object sender, EventArgs e)
{
control.IndicatorValue = (float)numericUpDown1.Value;
}
}
}
Here is a minimal example of the gauge class (I've removed most of the rendering):
using System;
using System.Windows.Forms;
using System.Diagnostics;
using System.Collections.Generic;
using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.DirectWrite;
using DXGI = SharpDX.DXGI; // Direct X Graphics Infrastructure
using D3D = SharpDX.Direct3D;
using D2D1 = SharpDX.Direct2D1;
using D3D11 = SharpDX.Direct3D11;
using DWrite = SharpDX.DirectWrite;
namespace SharpDXGauge
{
public partial class Gauge : UserControl
{
#region Members
private DirectX11Renderer renderer;
private float max;
private float min;
private float indicatorValue;
private Point topLeft;
private Point bottomRight;
private float height;
private float width;
Point valueTextTopLeft;
Point valueLabelOutlineTopLeft;
Point valueLabelOutlineBottomRight;
#endregion
public Gauge(float max, float min)
{
InitializeComponent();
renderer = new DirectX11Renderer(this);
this.max = max;
this.min = min;
this.indicatorValue = min;
renderer.SetBackgroundColor(renderer.SystemDrawingColorToSharpDXColor(this.BackColor));
DrawStaticContent();
DrawDynamicContent();
}
#region Drawing
private void CalculateSizes()
{
// Static
topLeft = new Point() { x = 25, y = 10 };
bottomRight = new Point() { x = topLeft.x + 20, y = this.Height - 50 };
height = bottomRight.y - topLeft.y;
width = bottomRight.x - topLeft.x;
// Dynamic
string valueText = indicatorValue.ToString();
Size2F valueTextSize = renderer.GetTextSize(valueText, 9.75f);
valueTextTopLeft = new Point() { x = bottomRight.x + 15, y = bottomRight.y - height * indicatorValue / (max - min) - (valueTextSize.Height / 2) };
}
private void DrawStaticContent()
{
CalculateSizes();
renderer.ClearLayer(TargetLayer.Static, Color.Transparent);
renderer.DrawRectangle(TargetLayer.Static, topLeft, bottomRight, Color.Black);
DrawDynamicContent();
renderer.PresentFrame();
}
private void DrawDynamicContent()
{
renderer.ClearLayer(TargetLayer.Dynamic, Color.Transparent);
CalculateSizes();
renderer.DrawText(TargetLayer.Dynamic, indicatorValue.ToString(), Color.White, valueTextTopLeft, 9.75f);
renderer.DrawRoundedRectangle(TargetLayer.Dynamic, valueLabelOutlineTopLeft, valueLabelOutlineBottomRight, 20, Color.Black);
renderer.PresentFrame();
}
#endregion
#region Properties
public float IndicatorValue
{
get
{
return indicatorValue;
}
set
{
if (indicatorValue != value)
{
indicatorValue = value;
DrawDynamicContent();
}
}
}
#endregion
#region Disposal
public new void Dispose()
{
renderer.Dispose();
}
#endregion
}
}
And my DirectX11Renderer class, I've removed all the unnecessary drawing methods etc:
using System;
using System.Windows.Forms;
using System.Diagnostics;
using System.Collections.Generic;
using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.DirectWrite;
using DXGI = SharpDX.DXGI; // Direct X Graphics Infrastructure
using D3D = SharpDX.Direct3D;
using D2D1 = SharpDX.Direct2D1;
using D3D11 = SharpDX.Direct3D11;
using DWrite = SharpDX.DirectWrite;
using DMaths = SharpDX.Mathematics.Interop;
namespace SharpDXGauge
{
public enum TargetLayer
{
Static,
Dynamic,
None
}
public struct Point
{
public float x;
public float y;
}
public partial class DirectX11Renderer : IDisposable
{
private Surface backBuffer;
private D3D11.Device d3dDevice;
private D3D11.Device1 d3dDevice1;
private DXGI.Device2 dxgiDevice2;
private D2D1.Device d2dDevice;
private SwapChain1 swapChain1;
private D2D1.Factory d2dFactory;
private DXGI.Factory2 dxgiFactory2;
private Adapter dxgiAdapter;
private RenderTarget d2dRenderTarget;
private DWrite.Factory dWriteFactory;
private Layer staticLayer;
private Layer dynamicLayer;
SolidColorBrush solidColorBrush;
//RadialGradientBrush radialGradientBrush;
//LinearGradientBrush linearGradientBrush;
private Color lastBackColour;
private bool isDrawing;
private LayerParameters layerParams;
private bool isStaticLayerPushed;
private bool isDynamicLayerPushed;
List<double> renderTimesMsec = new List<double>();
public DirectX11Renderer(Control surfaceToDrawOn)
{
// There seems to be endless ways to set up the different objects required for rendering
// This is the most efficient I've found that get's everything you need for 2d rendering
SwapChainDescription1 description = new SwapChainDescription1()
{
Width = surfaceToDrawOn.Width,
Height = surfaceToDrawOn.Height,
Format = Format.R8G8B8A8_UNorm,
Stereo = false,
SampleDescription = new SampleDescription(1, 0),
Usage = Usage.BackBuffer | Usage.RenderTargetOutput,
BufferCount = 2,
Scaling = Scaling.None,
SwapEffect = SwapEffect.FlipSequential,
};
d3dDevice = new D3D11.Device(DriverType.Hardware, DeviceCreationFlags.BgraSupport);
d3dDevice1 = d3dDevice.QueryInterface<D3D11.Device1>();
dxgiDevice2 = d3dDevice1.QueryInterface<DXGI.Device2>();
d2dDevice = new D2D1.Device(dxgiDevice2);
dxgiAdapter = dxgiDevice2.Adapter;
d2dFactory = new D2D1.Factory(D2D1.FactoryType.SingleThreaded);
dxgiFactory2 = dxgiAdapter.GetParent<DXGI.Factory2>();
swapChain1 = new SwapChain1(dxgiFactory2, d3dDevice1, surfaceToDrawOn.Handle, ref description);
backBuffer = swapChain1.GetBackBuffer<Surface>(0);
d2dRenderTarget = new RenderTarget(d2dFactory, backBuffer,
new RenderTargetProperties(new PixelFormat(Format.R8G8B8A8_UNorm, D2D1.AlphaMode.Premultiplied)));
staticLayer = new Layer(d2dRenderTarget);
dynamicLayer = new Layer(d2dRenderTarget);
dWriteFactory = new DWrite.Factory(DWrite.FactoryType.Shared);
layerParams = new LayerParameters()
{
ContentBounds = RectangleF.Infinite,
Opacity = 1
};
solidColorBrush = new SolidColorBrush(d2dRenderTarget, Color.White);
}
public void PresentFrame()
{
StartDrawingAndSwitchLayer(TargetLayer.None, false);
swapChain1.Present(0, PresentFlags.None);
}
public void SetBackgroundColor(Color colour)
{
StartDrawingAndSwitchLayer(TargetLayer.None);
lastBackColour = colour;
d2dRenderTarget.Clear(colour);
}
public void ClearLayer(TargetLayer layer, Color fillColorAfterClear)
{
StartDrawingAndSwitchLayer(layer);
d2dRenderTarget.Clear(fillColorAfterClear);
}
public void DrawRectangle(TargetLayer layer, Point topLeft, Point bottomRight, Color colour, float strokeWidth = 1)
{
StartDrawingAndSwitchLayer(layer);
solidColorBrush.Color = colour;
d2dRenderTarget.DrawRectangle(new DMaths.RawRectangleF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y), solidColorBrush, strokeWidth);
}
public void DrawRoundedRectangle(TargetLayer layer, Point topLeft, Point bottomRight, float roundingRadius, Color colour, float strokeWidth = 1)
{
StartDrawingAndSwitchLayer(layer);
solidColorBrush.Color = colour;
RoundedRectangle roundedRect = new RoundedRectangle();
roundedRect.Rect = new DMaths.RawRectangleF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
roundedRect.RadiusX = roundingRadius;
roundedRect.RadiusY = roundingRadius;
d2dRenderTarget.DrawRoundedRectangle(roundedRect, solidColorBrush, strokeWidth);
}
public void DrawText(TargetLayer layer, string text, Color colour, Point topLeft, float fontSize, FontWeight weight = FontWeight.Normal, FontStyle style = FontStyle.Normal, string font = "Microsoft Sans Serif")
{
StartDrawingAndSwitchLayer(layer);
solidColorBrush.Color = colour;
TextFormat format = new TextFormat(dWriteFactory, font, weight, style, FontStretch.Normal, fontSize);
Size2F textSize = GetTextSize(text, fontSize, 100, 100);
Point bottomRight = new Point() { x = topLeft.x + (int)Math.Ceiling(textSize.Width), y = topLeft.y + (int)Math.Ceiling(textSize.Height) };
d2dRenderTarget.DrawText(text, format,
new DMaths.RawRectangleF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y),
solidColorBrush);
}
public Size2F GetTextSize(string text, float fontSize, float maxWidth = 100000, float maxHeight = 100000, FontWeight weight = FontWeight.Normal, FontStyle style = FontStyle.Normal, string font = "Microsoft Sans Serif")
{
TextFormat format = new TextFormat(dWriteFactory, font, weight, style, FontStretch.Normal, fontSize);
TextLayout layout = new TextLayout(dWriteFactory, text, format, maxWidth, maxHeight);
return new Size2F(layout.Metrics.Width, layout.Metrics.Height);
}
public Color SystemDrawingColorToSharpDXColor(System.Drawing.Color colour)
{
Color toRtn = new Color(new byte[4] { colour.R, colour.G, colour.B, colour.A, });
return toRtn;
}
private void StartDrawingAndSwitchLayer(TargetLayer layer, bool draw = true)
{
if (draw && !isDrawing)
{
d2dRenderTarget.BeginDraw();
isDrawing = true;
}
switch (layer)
{
case TargetLayer.Static:
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
if (!isStaticLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, staticLayer);
isStaticLayerPushed = true;
}
break;
case TargetLayer.Dynamic:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (!isDynamicLayerPushed)
{
d2dRenderTarget.PushLayer(ref layerParams, dynamicLayer);
isDynamicLayerPushed = true;
}
break;
case TargetLayer.None:
if (isStaticLayerPushed)
{
d2dRenderTarget.PopLayer();
isStaticLayerPushed = false;
}
if (isDynamicLayerPushed)
{
d2dRenderTarget.PopLayer();
isDynamicLayerPushed = false;
}
break;
}
if (!draw && isDrawing)
{
d2dRenderTarget.EndDraw();
isDrawing = false;
}
}
public void Dispose()
{
// Release all resources
backBuffer.Dispose();
d3dDevice.Dispose();
d3dDevice1.Dispose();
dxgiDevice2.Dispose();
d2dDevice.Dispose();
swapChain1.Dispose();
d2dFactory.Dispose();
dxgiFactory2.Dispose();
dxgiAdapter.Dispose();
d2dRenderTarget.Dispose();
staticLayer.Dispose();
dynamicLayer.Dispose();
}
}
}
Putting this together gives a minimal example of what I'm seeing, I've removed code handling form resizing so with this, the bottom half appears black to begin with, but using a numerical updown on the form to update the IndicatorValue property creates the alternating all black/ what I want to see frames.
I've been trying to ebug this for a while, I'm beginning to wonder if it's to do with how I've set up my swap chain? At first I thought it must be how I'm handing my layers, and perhaps somehow every other change and dynamic content render it's not popping the layer and only showing the content of the dynamic layer? But stepping through each update has it pushing and popping the layers correctly as far as I can see, and generally not doing it right throws an exception.
Sorry this is so long,
Joe.
I'm trying to figure out how to auto scroll a scrollview when a user drags an item to the edge of the screen. I'm really not getting the behavious I expect out of this code. Does anyone have an example or a technique that works?
I have this in my:
onDrag(View v, DragEvent e)
{
case DragAction.Location:
var currentPosition = (int)e.GetX();
var point = GetTouchPositionFromDragEvent(v, e);
System.Diagnostics.Debug.WriteLine($"DragAction.Location from {v.GetType()} => {currentPosition}");
System.Diagnostics.Debug.WriteLine($"DragAction.GLOBAL from {v.GetType()} => {point.X}");
if (point.X > App.AppScreenWidth - 50)
{
_hostScrollView.ScrollToAsync(_hostScrollView.ScrollX + 30, 0, true);
}
if (point.X < 50)
{
_hostScrollView.ScrollToAsync(_hostScrollView.ScrollX - 30, 0, true);
}
}
public static Point GetTouchPositionFromDragEvent(View item, DragEvent e)
{
Rect rItem = new Rect();
item.GetGlobalVisibleRect(rItem);
return new Point(rItem.Left + (int)Math.Round(e.GetX()), rItem.Top + (int)Math.Round(e.GetY()));
}
This also has the knock on effect of only scrolling in one direction strangely and also requires the user to keep moving the item in order to fire the events which leads me to think this is entirely the wrong place to be even trying to do this scrolling.
Any pointers or nudges in the right direction would be really appreciated.
In your Activity
public class MainActivity : AppCompatActivity, View.IOnDragListener,View.IOnScrollChangeListener
private int mScrollDistance;
ScrollView _hostScrollView= FindViewById<ScrollView>(Resource.Id.xxx);
_hostScrollView.SetOnScrollChangeListener(this);
public bool OnDrag(View v, DragEvent e)
{
var action = e.Action;
var scrollView = v as ScrollView;
switch (action)
{
case DragAction.Started:
break;
case DragAction.Location:
int y = Java.Lang.Math.Round(e.GetY());
int translatedY = y - mScrollDistance;
int threshold = 50;
// make a scrolling up due the y has passed the threshold
if (translatedY < threshold)
{
// make a scroll up by 30 px
scrollView.ScrollBy(0, -30);
}
// make a autoscrolling down due y has passed the 500 px border
if (translatedY + threshold > 500)
{
// make a scroll down by 30 px
scrollView.ScrollBy(0, 30);
}
break;
}
return true;
}
public void OnScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY)
{
var scrollView = v as ScrollView;
mScrollDistance = scrollView.ScrollY;
}
You could modify the logic as you want if I misunderstand your requirement .
I'm trying to make a horizontal UIStackView with 2 images and a label:
The first image ico_qol should be anchored to the left side of the view.
The second image ico_edit should be anchored to the right side of the view.
The label should be anchored in between the two images.
The view should be sized to the height of the images which are the same. It should span the entire screen width.
What I'm trying to achieve is something like this (selected elements in blue):
Here is my code:
public partial class ViewController : UIViewController
{
public ViewController(IntPtr handle) : base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
UIStackView header = new UIStackView();
header.Axis = UILayoutConstraintAxis.Horizontal;
View.AddSubview(header);
//< color name = "healthy_green" >#409900</color>
var healthy_green = new UIColor(red: 0.25f, green: 0.60f, blue: 0.00f, alpha: 1.00f);
//< color name = "optimistic_orange" >#FF5F00</color>
var optimistic_orange = new UIColor(red: 1.00f, green: 0.37f, blue: 0.00f, alpha: 1.00f);
// Enable Auto Layout
header.TranslatesAutoresizingMaskIntoConstraints = false;
var ic_qol_image = UIImage.FromBundle("ic_qol");
ic_qol_image.ApplyTintColor(healthy_green);
var ic_qol = new UIImageView(ic_qol_image);
ic_qol.SizeToImage();
var qol_title = new UILabel { TranslatesAutoresizingMaskIntoConstraints = false, Text = #"Quality of Life" };
var ic_edit_image = UIImage.FromBundle("ic_edit");
ic_edit_image.ApplyTintColor(optimistic_orange);
var ic_edit = new UIImageView(ic_edit_image);
ic_edit.SizeToImage();
var qolHeaderViews = new UIView[] { ic_qol, qol_title, ic_edit };
header.AddSubviews(qolHeaderViews);
header.Anchor(top: View.SafeAreaLayoutGuide.TopAnchor, leading: View.LeadingAnchor, trailing: View.TrailingAnchor);
ic_qol.Anchor(top: header.TopAnchor, leading: header.LeadingAnchor);
ic_edit.Anchor(top: header.TopAnchor, trailing: header.TrailingAnchor);
qol_title.Anchor(top: View.SafeAreaLayoutGuide.TopAnchor, leading: ic_qol.RightAnchor, trailing: ic_edit.RightAnchor, padding: new UIEdgeInsets(0, 10, 0, 0)); <=== fails here.
}
public override void DidReceiveMemoryWarning()
{
base.DidReceiveMemoryWarning();
// Release any cached data, images, etc that aren't in use.
}
}
internal static class extensions
{
// https://stackoverflow.com/a/48601685/15186
internal static void SizeToImage(this UIImageView uIImageView)
{
//Grab loc
var xC = uIImageView.Center.X;
var yC = uIImageView.Center.Y;
//Size to fit
uIImageView.Frame = new CGRect(x: 0, y: 0, width: uIImageView.Image.Size.Width / 2, height: uIImageView.Image.Size.Height / 2);
//Move to loc
uIImageView.Center = new CGPoint(x: xC, y: yC);
}
internal static void FillParentView(this UIView uIView)
{
uIView.Anchor(top: uIView.Superview?.TopAnchor, leading: uIView.Superview?.LeadingAnchor, bottom: uIView.Superview?.BottomAnchor, trailing: uIView.Superview.TrailingAnchor);
}
internal static void AnchorSize(this UIView uIView, UIView to)
{
uIView.WidthAnchor.ConstraintEqualTo(to.WidthAnchor).Active = true;
uIView.HeightAnchor.ConstraintEqualTo(to.HeightAnchor).Active = true;
}
internal static void Anchor(this UIView uIView, NSLayoutYAxisAnchor top = null, NSLayoutXAxisAnchor leading = null, NSLayoutYAxisAnchor bottom = null, NSLayoutXAxisAnchor trailing = null, UIEdgeInsets padding = default, CGSize size= default)
{
uIView.TranslatesAutoresizingMaskIntoConstraints = false;
if (top != null)
{
uIView.TopAnchor.ConstraintEqualTo(top, padding.Top).Active = true;
}
if (leading != null)
{
uIView.LeadingAnchor.ConstraintEqualTo(leading, padding.Left).Active = true; <=== fails here.
}
if (bottom != null)
{
uIView.BottomAnchor.ConstraintEqualTo(bottom, -padding.Bottom).Active = true;
}
if (trailing != null)
{
uIView.TrailingAnchor.ConstraintEqualTo(trailing, -padding.Right).Active = true;
}
if ( size.Width != 0)
{
uIView.WidthAnchor.ConstraintEqualTo(size.Width).Active = true;
}
if ( size.Height != 0)
{
uIView.HeightAnchor.ConstraintEqualTo(size.Height).Active = true;
}
}
}
I'm trying to constraint the Leading anchor of the Label to the trailing anchor of the image but it fails with the following exception:
A constraint cannot be made between <NSLayoutXAxisAnchor:0x280bcb980 "UILabel:0x1015132a0'Quality of Life'.leading">' and '<NSLayoutXAxisAnchor:0x280bcb840 "UIImageView:0x101510ea0.right">' because their units are not compatible.
I understand that their units may be incompatible but how can I do that anyways? How to make the units compatible?
Credits: The extensions methods I'm using are originally created during this video tutorial. I ported them to c#.
You can achieve this layout easily with a "horizontal" stack view.
Note: I don't use xamarin / c#, so I'll just describe what you want to do.
create the stack view
set its properties to
Axis: Horizontal
Distribution: Fill
Alignment: Center (this is the vertical alignment of the subviews)
Spacing: 8 (you probably want a little space between subviews)
create 2 image views
load an image into each image view
set width and height constraints on the image views as desired (it looks like you are using 1/2 the height and width of your images?)
create a label
add the image views and label to the stack view as arranged subviews
header.AddArrangedSubview(ic_qol);
header.AddArrangedSubview(qol_title);
header.AddArrangedSubview(ic_edit);
add the stack view to the view
View.AddSubview(header);
The last step will be to add constraints to put the stack view at the top, spanning the width of the view:
// Get the parent view's layout
var margins = View.LayoutMarginsGuide;
// Pin the top edge of the stackView to the margin
header.TopAnchor.ConstraintEqualTo (margins.TopAnchor).Active = true;
// Pin the leading edge of the stackView to the margin
header.LeadingAnchor.ConstraintEqualTo (margins.LeadingAnchor).Active = true;
// Pin the trailing edge of the stackView to the margin
header.TrailingAnchor.ConstraintEqualTo (margins.TrailingAnchor).Active = true;
The result should look about like this:
Each imageView is constrained to 40-pts width and height-equalTo-width. The label has a cyan background to make it easy to see the layout, and text alignment is center. The outlines are just there so we can see the stackView's frame.
and at runtime, without the frame outlines:
Is it possible to change the constraints of a RelativeLayout after they have been set one time?
In the documentation I see methods like GetBoundsConstraint(BindableObject), but I think you can only use them if you have a XAML file. For now I'm trying to do this in code. I get null if I try
RelativeLayout.GetBoundsConstraint(this.label);
The only way I found out is to remove the children from the layout and add it with the new constraints again.
Example:
public class TestPage : ContentPage
{
private RelativeLayout relativeLayout;
private BoxView view;
private bool moreWidth = false;
public TestPage()
{
this.view = new BoxView
{
BackgroundColor = Color.Yellow,
};
Button button = new Button
{
Text = "change",
TextColor = Color.Black,
};
button.Clicked += Button_Clicked;
this.relativeLayout = new RelativeLayout();
this.relativeLayout.Children.Add(this.view,
Constraint.Constant(0),
Constraint.Constant(0),
Constraint.Constant(80),
Constraint.RelativeToParent((parent) =>
{
return parent.Height;
}));
this.relativeLayout.Children.Add(button,
Constraint.RelativeToParent((parent) =>
{
return parent.Width / 2;
}),
Constraint.RelativeToParent((parent) =>
{
return parent.Height / 2;
}));
this.Content = this.relativeLayout;
}
private void Button_Clicked(object sender, EventArgs e)
{
double width = 0;
if(this.moreWidth)
{
width = 120;
}
else
{
width = 80;
}
var c= BoundsConstraint.FromExpression((Expression<Func<Rectangle>>)(() => new Rectangle(0, 0, width, this.Content.Height)), new View[0]);
RelativeLayout.SetBoundsConstraint(this.view, c);
this.relativeLayout.ForceLayout();
this.moreWidth = !this.moreWidth;
}
}
It does not officially possible with the current version of Xamarin Forms. The RelativeLayout container only recomputes constraints when adding/removing items from its children collection (it caches the solved constraints - presumable for performance). Even though the various constraints are implemented as Bindable Properties, they still do not get recomputed when changed.
I assume that the intention is to someday respect constraint updates, which would be useful with animations for example, but for now it doesn't appear to work that way.
HOWEVER, I took a look at the decompiled source for RelativeLayout and it is possible to hack together a way around it - but it might not suit your needs, depending on how much functionality you require and how complex your constraint definitions are.
See this example code (the key part is setting the constraint using SetBoundsConstraint, which overrides the internally computed bounds of the added view - and then calling ForceLayout()):
public partial class App : Application
{
public App ()
{
var label = new Label {
Text = "Test",
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
BackgroundColor = Color.Silver
};
var layout = new RelativeLayout ();
layout.Children.Add (label,
Constraint.Constant (50),
Constraint.Constant (100),
Constraint.Constant (260),
Constraint.Constant (30));
MainPage = new ContentPage {
Content = layout
};
var fwd = true;
layout.Animate ("bounce",
(delta) => {
var d = fwd ? delta : 1.0 - delta;
var y = 100.0 + (50.0 * d);
var c = BoundsConstraint.FromExpression ((Expression<Func<Rectangle>>)(() => new Rectangle (50, y, 260, 30)), new View [0]);
RelativeLayout.SetBoundsConstraint(label, c);
layout.ForceLayout ();
}, 16, 800, Easing.SinInOut, (f, b) => {
// reset direction
fwd = !fwd;
}, () => {
// keep bouncing
return true;
});
}
}
Yes. This possible.
Layout code:
<StackLayout RelativeLayout.XConstraint="{Binding XConstaint}" ...>
VM code:
public Constraint XConstaint
{
get => _xConstaint;
set { SetFieldValue(ref _xConstaint, value, nameof(XConstaint)); }
}
public override void OnAppearing()
{
base.OnAppearing();
XConstaint = Constraint.RelativeToParent((parent) => { return parent.Width - 128; });
}
If i understood you correctly , I would try the following
Implement SizeChanged event for relativeLayout
like :-
var relativeLayout = new RelativeLayout ();
relativeLayout.SizeChanged += someEventForTesting;
and invoke the event whenever you want to resize the relative layout or some particular child.
public void someEventForTesting(Object sender , EventArgs)
{
var layout = (RelativeLayout)sender ;
//here you have access to layout and every child , you can add them again with new constraint .
}