问题
I have a set of controls which I am stacking vertically inside a scrollable control.
Each control contains text (like message bubbles on an iPhone), which the bubble resizes based on the height of the text.
The problem I face, is when I resize the parent so it is smaller, the bubbles start to overlap, and when I resize so the bubbles are one-line, there is too much space in between each bubble.
What I would like to do, is to have each bubble snap the top of the bubble to 10pts off the bubble above it, the fastest way possible without any flicker (as there is presently no flicker on resize)
I have thought about embedding each control into another parent (eg, a grid control row), but then each bubble added would be responsible for resizing the parent of itself, and then anchors would no longer work for their top, left, and right positioning.
How can this be done ? (sorry, the details of the question are above as it can't really be worded into a simple one liner question due to the complexity and specifics)
Thanks in advance :)
AS REQUESTED, SCREENSHOTS and CODE
This is the view normally
After Resizing, then scrolling down to controls that weren't in visible segment And resizing back, then scrolling back upNow the good stuff..... CODE.....
Here is the code for my custom control:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Runtime.InteropServices;
public class MessageControl : ScrollableControl {
public List<Message> Messages { get; private set; }
private Color _LeftBubbleColor=Color.FromArgb(217,217,217);
private Color _RightBubbleColor=Color.FromArgb(192,206,215);
private Color _LeftBubbleTextColor=Color.FromArgb(52,52,52);
private Color _RightBubbleTextColor=Color.FromArgb( 52, 52, 52 );
private bool _DrawArrow=true;
private int _BubbleIndent=40;
private int _BubbleSpacing=10;
public enum BubblePositionEnum { Left, Right }
public Color LeftBubbleColor { get { return _LeftBubbleColor; } set {_LeftBubbleColor = value; } }
public Color RightBubbleColor { get { return _RightBubbleColor; } set { _RightBubbleColor=value; } }
public Color LeftBubbleTextColor { get { return _LeftBubbleTextColor; } set { _LeftBubbleTextColor=value; } }
public Color RightBubbleTextColor { get { return _RightBubbleTextColor; } set { _RightBubbleTextColor=value; } }
public int BubbleIndent { get { return _BubbleIndent; } set { _BubbleIndent = value; } }
public int BubbleSpacing { get { return _BubbleSpacing; } set { _BubbleSpacing=value; } }
public bool DrawArrow { get { return _DrawArrow; } set { _DrawArrow = value; } }
public MessageControl() {
Messages = new List<Message>();
SetStyle( ControlStyles.AllPaintingInWmPaint|ControlStyles.OptimizedDoubleBuffer|ControlStyles.ResizeRedraw|ControlStyles.SupportsTransparentBackColor|ControlStyles.UserPaint, true );
DoubleBuffered=true;
BackColor=Color.Orange;
Anchor=AnchorStyles.Top|AnchorStyles.Left|AnchorStyles.Right|AnchorStyles.Bottom;
AutoScroll=true;
}
public void Remove( Message message ) {
this.Invalidate();
Messages.Remove( message );
RedrawControls();
}
public void Remove( Message[] messages ) {
this.Invalidate();
foreach ( Message m in messages ) {
Messages.Remove( m );
}
RedrawControls();
}
public void Add( string Message, BubblePositionEnum Position ) {
Message b = new Message(Position);
if ( Messages.Count>0 ) {
b.Top = Messages[Messages.Count-1].Top + Messages[Messages.Count-1].Height + _BubbleSpacing+AutoScrollPosition.Y;
} else {
b.Top = _BubbleSpacing+AutoScrollPosition.Y;
}
b.Text = Message;
b.DrawBubbleArrow=_DrawArrow;
if ( VerticalScroll.Visible ) {
b.Width=Width-( _BubbleIndent+_BubbleSpacing+SystemInformation.VerticalScrollBarWidth );
} else {
b.Width=Width-( _BubbleIndent+_BubbleSpacing );
}
if ( Position==BubblePositionEnum.Right ) {
b.Left = _BubbleIndent;
b.BubbleColor = _RightBubbleColor;
b.ForeColor = _RightBubbleTextColor;
} else {
b.Left = _BubbleSpacing;
b.BubbleColor=_LeftBubbleColor;
b.ForeColor=_LeftBubbleTextColor;
}
Messages.Add(b);
this.Controls.Add(b);
}
protected override void OnResize( System.EventArgs e ) {
RedrawControls();
base.OnResize( e );
}
private void RedrawControls() {
int count=0;
Message last=null;
int new_width=this.Width;
SuspendLayout();
foreach ( Message m in this.Controls ) {
if ( count>0 ) {
m.Top=last.Top+last.Height+_BubbleSpacing+AutoScrollPosition.Y;
if ( VerticalScroll.Visible ) {
m.Width=new_width-( _BubbleIndent+_BubbleSpacing+SystemInformation.VerticalScrollBarWidth );
} else {
m.Width=new_width-( _BubbleIndent+_BubbleSpacing );
}
}
last=m;
count++;
}
ResumeLayout();
Invalidate();
}
public class Message : Control {
private GraphicsPath Shape;
private Color _TextColor=Color.FromArgb( 52, 52, 52 );
private Color _BubbleColor=Color.FromArgb( 217, 217, 217 );
private bool _DrawBubbleArrow=true;
private BubblePositionEnum _BubblePosition = BubblePositionEnum.Left;
public override Color ForeColor { get { return this._TextColor; } set { this._TextColor=value; this.Invalidate(); } }
public BubblePositionEnum BubblePosition { get { return this._BubblePosition; } set { this._BubblePosition=value; this.Invalidate(); } }
public Color BubbleColor { get { return this._BubbleColor; } set { this._BubbleColor=value; this.Invalidate(); } }
public bool DrawBubbleArrow { get { return _DrawBubbleArrow; } set { _DrawBubbleArrow=value; Invalidate(); } }
public Message(BubblePositionEnum Position) {
_BubblePosition=Position;
SetStyle( ControlStyles.AllPaintingInWmPaint|ControlStyles.OptimizedDoubleBuffer|ControlStyles.ResizeRedraw|ControlStyles.SupportsTransparentBackColor|ControlStyles.UserPaint, true );
DoubleBuffered=true;
Size=new Size( 152, 38 );
BackColor=Color.Transparent;
ForeColor=Color.FromArgb( 52, 52, 52 );
Font=new Font( "Segoe UI", 10 );
Anchor=AnchorStyles.Top|AnchorStyles.Left|AnchorStyles.Right;
}
protected override void OnResize( System.EventArgs e ) {
Shape=new GraphicsPath();
var _Shape=Shape;
if ( BubblePosition==BubblePositionEnum.Left ) {
_Shape.AddArc( 9, 0, 10, 10, 180, 90 );
_Shape.AddArc( Width-11, 0, 10, 10, -90, 90 );
_Shape.AddArc( Width-11, Height-11, 10, 10, 0, 90 );
_Shape.AddArc( 9, Height-11, 10, 10, 90, 90 );
} else {
_Shape.AddArc( 0, 0, 10, 10, 180, 90 );
_Shape.AddArc( Width-18, 0, 10, 10, -90, 90 );
_Shape.AddArc( Width-18, Height-11, 10, 10, 0, 90 );
_Shape.AddArc( 0, Height-11, 10, 10, 90, 90 );
}
_Shape.CloseAllFigures();
Invalidate();
base.OnResize( e );
}
protected override void OnPaint( PaintEventArgs e ) {
base.OnPaint( e );
Bitmap B=new Bitmap( this.Width, this.Height );
Graphics G=Graphics.FromImage( B );
SizeF s=G.MeasureString( Text, Font, Width-25 );
this.Height=(int)( Math.Floor( s.Height )+10 );
B=new Bitmap( this.Width, this.Height );
G=Graphics.FromImage( B );
var _G=G;
_G.SmoothingMode=SmoothingMode.HighQuality;
_G.PixelOffsetMode=PixelOffsetMode.HighQuality;
_G.Clear( BackColor );
// Fill the body of the bubble with the specified color
_G.FillPath( new SolidBrush( _BubbleColor ), Shape );
// Draw the string specified in 'Text' property
if ( _BubblePosition==BubblePositionEnum.Left ) {
_G.DrawString( Text, Font, new SolidBrush( ForeColor ), new Rectangle( 13, 4, Width-25, Height-5 ) );
} else {
_G.DrawString( Text, Font, new SolidBrush( ForeColor ), new Rectangle( 5, 4, Width-25, Height-5 ) );
}
// Draw a polygon on the right side of the bubble
if ( _DrawBubbleArrow==true ) {
if(_BubblePosition == BubblePositionEnum.Left) {
Point[] p = {
new Point(9, 9),
new Point(0, 15),
new Point(9, 20)
};
_G.FillPolygon( new SolidBrush( _BubbleColor ), p );
_G.DrawPolygon( new Pen( new SolidBrush( _BubbleColor ) ), p );
} else {
Point[] p = {
new Point(Width - 8, 9),
new Point(Width, 15),
new Point(Width - 8, 20)
};
_G.FillPolygon( new SolidBrush( _BubbleColor ), p );
_G.DrawPolygon( new Pen( new SolidBrush( _BubbleColor ) ), p );
}
}
G.Dispose();
e.Graphics.InterpolationMode=InterpolationMode.HighQualityBicubic;
e.Graphics.DrawImageUnscaled( B, 0, 0 );
B.Dispose();
}
}
}
And for my manifest :
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- UAC Manifest Options
If you want to change the Windows User Account Control level replace the
requestedExecutionLevel node with one of the following.
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
Specifying requestedExecutionLevel node will disable file and registry virtualization.
If you want to utilize File and Registry Virtualization for backward
compatibility then delete the requestedExecutionLevel node.
-->
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of all Windows versions that this application is designed to work with.
Windows will automatically select the most compatible environment.-->
<!-- If your application is designed to work with Windows Vista, uncomment the following supportedOS node-->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"></supportedOS>
<!-- If your application is designed to work with Windows 7, uncomment the following supportedOS node-->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- If your application is designed to work with Windows 8, uncomment the following supportedOS node-->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"></supportedOS>
<!-- If your application is designed to work with Windows 8.1, uncomment the following supportedOS node-->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
</application>
</compatibility>
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
<!-- <dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>-->
<asmv1:application>
<asmv1:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv1:windowsSettings>
</asmv1:application>
</asmv1:assembly>
And for the form itself to demo the control on
int x = 0;
while ( x<20 ) {
messageControl1.Add( "Testing", MessageControl.BubblePositionEnum.Right );
messageControl1.Add( "Testing", MessageControl.BubblePositionEnum.Right );
messageControl1.Add( "Testing", MessageControl.BubblePositionEnum.Left );
x++;
}
The form is set to scale to DPI (which is correct, so change that when you edit to test, and use my manifest as that is DPI scaling, not Font scaling).
回答1:
I think found it.
Just add this line in the Redraw function to realize only 6 object where being updated
Debug.WriteLine(m.Name + "-" + m.Top + "-" + m.Width);
-10-234
-58-217
-106-217
-154-217
-202-217
-250-217
first bug
And this line in the test method fix the creation process
messageControl1.SuspendLayout(); //add
while (x < 20)
{
messageControl1.Add("Testing", MessageControl.BubblePositionEnum.Right);
messageControl1.Add("Testing", MessageControl.BubblePositionEnum.Right);
messageControl1.Add("Testing", MessageControl.BubblePositionEnum.Left);
x++;
}
messageControl1.ResumeLayout(); //add
messageControl1.Invalidate(); //add
As you can see the scroll is at the end.
second bug
Looks like hide elements have diferent size, you can see the debug result.
So I just save firt element Height and assign to everyone.
Debug.WriteLine("------------------------------------------------");
int firstHeight = 0;
foreach (Message m in this.Controls)
{
if (count > 0)
{
Debug.WriteLine(m.Height);
m.Height = firstHeight;
m.Top = last.Top + firstHeight + _BubbleSpacing + AutoScrollPosition.Y;
if (VerticalScroll.Visible)
{
m.Width = new_width - (_BubbleIndent + _BubbleSpacing + SystemInformation.VerticalScrollBarWidth);
}
else
{
m.Width = new_width - (_BubbleIndent + _BubbleSpacing);
}
}
else
{
firstHeight = m.Height;
}
Debug.WriteLine(m.Name + "-" + m.Top + "-" + m.Width);
last = m;
count++;
}
回答2:
Here's what I'm talking about doing it via the FlowLayoutPanel. Take a close look at all of the code as I've made significant changes throughout. I recommend pasting this over a blank project to play with it.
The form when it initially loads:
The form after being resized smaller:
Here's the form now scrolled to the bottom to show that the FlowLayoutPanel has taken care of re-arranging everything for me:
The re-worked code:
public partial class Form1 : Form
{
public Form1()
{
this.InitializeComponent();
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor | ControlStyles.UserPaint, true);
this.UpdateStyles();
}
private void Form1_Load(object sender, EventArgs e)
{
for (int x = 0; x < 20; x++)
{
messageControl1.Add(x.ToString("00") + ": Testing testing testing ...", MessageControl.BubblePositionEnum.Right);
messageControl1.Add(x.ToString("00") + ": Testing with variable length strings. This one is longer!", MessageControl.BubblePositionEnum.Right);
messageControl1.Add(x.ToString("00") + ": Testing is fun.", MessageControl.BubblePositionEnum.Left);
}
}
}
public class MessageControl : FlowLayoutPanel
{
public List<Message> Messages { get; private set; }
private Color _LeftBubbleColor = Color.FromArgb(217, 217, 217);
private Color _RightBubbleColor = Color.FromArgb(192, 206, 215);
private Color _LeftBubbleTextColor = Color.FromArgb(52, 52, 52);
private Color _RightBubbleTextColor = Color.FromArgb(52, 52, 52);
private bool _DrawArrow = true;
private int _BubbleIndent = 40;
private int _BubbleSpacing = 10;
public enum BubblePositionEnum { Left, Right }
public Color LeftBubbleColor { get { return _LeftBubbleColor; } set { _LeftBubbleColor = value; } }
public Color RightBubbleColor { get { return _RightBubbleColor; } set { _RightBubbleColor = value; } }
public Color LeftBubbleTextColor { get { return _LeftBubbleTextColor; } set { _LeftBubbleTextColor = value; } }
public Color RightBubbleTextColor { get { return _RightBubbleTextColor; } set { _RightBubbleTextColor = value; } }
public int BubbleIndent { get { return _BubbleIndent; } set { _BubbleIndent = value; } }
public int BubbleSpacing { get { return _BubbleSpacing; } set { _BubbleSpacing = value; } }
public bool DrawArrow { get { return _DrawArrow; } set { _DrawArrow = value; } }
public MessageControl()
{
this.Messages = new List<Message>();
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor | ControlStyles.UserPaint, true);
this.UpdateStyles();
this.BackColor = Color.Orange;
this.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
this.AutoScroll = true;
}
public void Remove(Message message)
{
this.Messages.Remove(message);
this.Controls.Remove(message);
this.Invalidate();
this.Refresh();
}
public void Remove(Message[] messages)
{
this.SuspendLayout();
foreach (Message m in messages)
{
Messages.Remove(m);
this.Controls.Remove(m);
}
this.ResumeLayout();
this.Invalidate();
this.Refresh();
}
public void Add(string Message, BubblePositionEnum Position)
{
Message b = new Message(this, Message, Position);
b.DrawBubbleArrow = _DrawArrow;
b.Width = this.ClientSize.Width;
Messages.Add(b);
this.Controls.Add(b);
}
protected override void OnLayout(LayoutEventArgs levent)
{
this.ResizeMessages();
base.OnLayout(levent);
}
protected override void OnResize(System.EventArgs e)
{
this.ResizeMessages();
base.OnResize(e);
}
private void ResizeMessages()
{
this.SuspendLayout();
foreach (Message m in this.Messages)
{
m.Width = this.ClientSize.Width - 9;
}
this.ResumeLayout();
}
public class Message : Control
{
private MessageControl _mc;
private GraphicsPath Shape;
private Color _TextColor = Color.FromArgb(52, 52, 52);
private Color _BubbleColor = Color.FromArgb(217, 217, 217);
private bool _DrawBubbleArrow = true;
private BubblePositionEnum _BubblePosition = BubblePositionEnum.Left;
public override Color ForeColor { get { return this._TextColor; } set { this._TextColor = value; this.Invalidate(); } }
public BubblePositionEnum BubblePosition { get { return this._BubblePosition; } set { this._BubblePosition = value; this.Invalidate(); } }
public Color BubbleColor { get { return this._BubbleColor; } set { this._BubbleColor = value; this.Invalidate(); } }
public bool DrawBubbleArrow { get { return _DrawBubbleArrow; } set { _DrawBubbleArrow = value; Invalidate(); } }
private Message() { }
public Message(MessageControl mc, string Message, BubblePositionEnum Position)
{
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.SupportsTransparentBackColor | ControlStyles.UserPaint, true);
this.UpdateStyles();
this._mc = mc;
this._BubblePosition = Position;
this.Text = Message;
this.BubbleColor = (Position == BubblePositionEnum.Right ? mc.RightBubbleColor : mc.LeftBubbleColor);
this.BackColor = this.BubbleColor;
this.ForeColor = (Position == BubblePositionEnum.Right ? mc.RightBubbleTextColor : mc.LeftBubbleTextColor);
this.Font = new Font("Segoe UI", 10);
this.Size = new Size(152, 38);
this.Anchor = AnchorStyles.Left | AnchorStyles.Right;
}
protected override void OnResize(System.EventArgs e)
{
base.OnResize(e);
Shape = new GraphicsPath();
if (BubblePosition == BubblePositionEnum.Left)
{
Shape.AddArc(9, 0, 10, 10, 180, 90);
Shape.AddArc(Width - 10 - this._mc.BubbleIndent, 0, 10, 10, -90, 90);
Shape.AddArc(Width - 10 - this._mc.BubbleIndent, Height - 11, 10, 10, 0, 90);
Shape.AddArc(9, Height - 11, 10, 10, 90, 90);
}
else
{
Shape.AddArc(this._mc._BubbleIndent, 0, 10, 10, 180, 90);
Shape.AddArc(Width - 18, 0, 10, 10, -90, 90);
Shape.AddArc(Width - 18, Height - 11, 10, 10, 0, 90);
Shape.AddArc(this._mc._BubbleIndent, Height - 11, 10, 10, 90, 90);
}
if (_DrawBubbleArrow == true)
{
Point[] p;
if (_BubblePosition == BubblePositionEnum.Left)
{
p = new Point[] {
new Point(9, 9),
new Point(0, 15),
new Point(9, 20)
};
}
else
{
p = new Point[] {
new Point(Width - 8, 9),
new Point(Width, 15),
new Point(Width - 8, 20)
};
}
Shape.AddPolygon(p);
}
Shape.CloseAllFigures();
this.Region = new Region(Shape);
this.Invalidate();
this.Refresh();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var G = e.Graphics;
int RenderWidth = this.Width - 10 - this._mc.BubbleIndent;
SizeF s = G.MeasureString(Text, Font, RenderWidth);
this.Height = (int)(Math.Floor(s.Height) + 10);
G.SmoothingMode = SmoothingMode.HighQuality;
G.PixelOffsetMode = PixelOffsetMode.HighQuality;
G.InterpolationMode = InterpolationMode.HighQualityBicubic;
// Draw the string specified in 'Text' property
using (SolidBrush brush = new SolidBrush(this.ForeColor))
{
if (_BubblePosition == BubblePositionEnum.Left)
{
G.DrawString(Text, Font, brush, new Rectangle(13, 4, RenderWidth, Height - 5));
}
else
{
G.DrawString(Text, Font, brush, new Rectangle(this._mc.BubbleIndent + 5, 4, RenderWidth, Height - 5));
}
}
}
}
}
回答3:
In this case I'm creating a grid to represent a labyrinth.
My form have a Panel control, then I create buttons inside calculating Top, Left values. For me button size is fixed, you could use your parent value.
I name the buttons grid0102
for row = "01"and col="02"
Then select Location, Size and Text properties.
And finally BackColor for walls
int buttonSize = 20;
Panel myPanel = (Panel)this.Controls["panelArea"];
string[] myGrid = getGrid(0);
for (int row = 0; row < r; row++)
{
char[] rowChar = myGrid[row].ToCharArray();
for (int col = 0; col < c; col++)
{
Button newButton = new Button();
newButton.Name = "grid" + row.ToString("D3") + col.ToString("D3");
newButton.Location = new Point { X = buttonSize * col, Y = buttonSize * row };
newButton.Size = new Size { Width = buttonSize, Height = buttonSize };
newButton.Text = rowChar[col].ToString();
if (rowChar[col] == '%') newButton.BackColor = Color.Green;
myPanel.Controls.Add(newButton);
Debug.WriteLine(newButton.Location);
}
}
NOTE ADDED
But if the problem is handling resize, just encampsule that code in a function and call it when Resize event occurs.
private void panelArea_Resize(object sender, EventArgs e)
{
UI_Resize();
}
来源:https://stackoverflow.com/questions/30764357/control-snapping-on-resize-in-c-sharp-scrollable-container