I've got something like a serious problem.
I made a UserControl which is movable (like a window) in its parent, by using the MouseDown, MouseMove,MouseUp events .
The [Designer("System.Windows.Forms.Design.ParentControlDesigner, System.Design", typeof(IDesigner))] Attribute is used that I can add Controls to this UserControl in VS's Designer.
State:
Moving those UserControls works fine (the Usercontrol moves as expected...)
Controls can be added in VS's Designer and appear as designed in Runtime[visible, like it should be]
throught Moving the UserControl, the Children get invisible, but .Visible=true doesn't change
.BringToFront(); has no affect (I thought they might be behind the container)
Here's the UserControl class:
[Designer("System.Windows.Forms.Design.ParentControlDesigner, System.Design", typeof(IDesigner))]
public partial class MovableContainer : UserControl
{
bool mdown = false;
Point mpos;
[EditorBrowsable(EditorBrowsableState.Always)]
[SettingsBindable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Axis Rasta { get; set; }
public static int DefautlRasta = 10;
public MovableContainer()
{
rasta = DefautlRasta;
InitializeComponent();
this.MouseDown += ((object o, MouseEventArgs e) =>
{
mdown = true;
mpos = this.PointToClient(MousePosition);
});
this.MouseUp += ((object o, MouseEventArgs e) => mdown = false);
this.MouseMove += MovableContainer_MouseMove;
this.Paint += (object o, PaintEventArgs e) =>
{
Console.WriteLine("BTF");
this.Parent.Controls.OfType<Control>().ToList().ForEach(x => x.BringToFront());
this.Controls.OfType<Control>().ToList().ForEach(x => x.BringToFront());
this.Controls.OfType<Control>().ToList().ForEach(x => x.Show());
};
this.ParentChanged += ((object o, EventArgs e) =>
{
if (this.Parent == null)
{
try { this.Parent.SizeChanged -= Parent_SizeChanged; }
catch { }
}
else
{
try { this.Parent.SizeChanged += Parent_SizeChanged; }
catch { }
}
}
);
// this.KeyDown += ((object o, KeyEventArgs e) => {
///kdown = (RastaKey == e.KeyCode); Console.WriteLine("K:"+kdown);
//});
//this.KeyUp += ((object o, KeyEventArgs e) => kdown = false);
}
void Parent_SizeChanged(object sender, EventArgs e)
{
this.Boundis = new Rectangle(Parent.Padding.Left, Parent.Padding.Top, Parent.Size.Width - Parent.Padding.Horizontal, Parent.Size.Height - Parent.Padding.Vertical);
{
this.Location = this.Location.Add(this.PointToClient(MousePosition).Sub(mpos)).Rasta(Rasta, rasta);
Rectangle rct = new Rectangle(this.Location, this.Size);
if (this.Boundis.X > rct.X)
{
this.Location = new Point(this.Boundis.X, this.Location.Y);
Console.Write("R");
}
//left
if (this.Boundis.Right < rct.Right)
{
this.Location = new Point(this.Boundis.Right - rct.Width, rct.Y);
Console.Write("L");
}
//top
if (this.Boundis.Y > rct.Y)
{
this.Location = new Point(rct.X, this.Boundis.Y);
Console.Write("T");
}
//bottom
if (this.Boundis.Bottom < rct.Bottom)
{
this.Location = new Point(rct.X, this.Boundis.Bottom - rct.Height);
Console.Write("B");
}
Console.WriteLine();
}
}
void MovableContainer_MouseMove(object sender, MouseEventArgs e)
{
if (mdown)
{
this.Location = this.Location.Add(this.PointToClient(MousePosition).Sub(mpos)).Rasta(Rasta, rasta);
Rectangle rct = new Rectangle(this.Location, this.Size);
if (this.Boundis.X > rct.X)
{
this.Location = new Point(this.Boundis.X, this.Location.Y);
Console.Write("R");
}
//left
if (this.Boundis.Right < rct.Right)
{
this.Location = new Point(this.Boundis.Right - rct.Width, rct.Y);
Console.Write("L");
}
//top
if (this.Boundis.Y > rct.Y)
{
this.Location = new Point(rct.X, this.Boundis.Y);
Console.Write("T");
}
//bottom
if (this.Boundis.Bottom < rct.Bottom)
{
this.Location = new Point(rct.X, this.Boundis.Bottom - rct.Height);
Console.Write("B");
}
Console.WriteLine();
}
}
public Rectangle Boundis { get; set; }
}
public enum Axis { X, Y, None }
So, how can I fix this?
Frankly the code you posted is a big mess - most of the things you put there make no sense. From what I see, you are trying to implement run time movable container with clipping. A simple inherited Panel would do the same w/o the need of those designer attributes etc. Anyway, the problem you are describing is caused by the wrong calculations in your Parent_SizeChanged handler. Here is a partially cleaned up code that does what I think yours is trying to do w/o having any problems:
[Designer("System.Windows.Forms.Design.ParentControlDesigner, System.Design", typeof(IDesigner))]
public partial class MovableContainer : UserControl
{
bool mdown = false;
Point mpos;
int rasta;
Control parent;
[EditorBrowsable(EditorBrowsableState.Always)]
[SettingsBindable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Axis Rasta { get; set; }
public static int DefautlRasta = 10;
public MovableContainer()
{
rasta = DefautlRasta;
InitializeComponent();
this.MouseDown += (sender, e) =>
{
mdown = true;
mpos = e.Location;
};
this.MouseUp += (sender, e) =>
{
mdown = false;
};
this.MouseMove += (sender, e) =>
{
if (mdown)
SetLocation(this.Location.Add(e.Location.Sub(mpos)).Rasta(Rasta, rasta));
};
EventHandler onParentSizeChanged = (sender, e) =>
{
SetLocation(this.Location);
};
this.ParentChanged += (sender, e) =>
{
if (parent != null) parent.SizeChanged -= onParentSizeChanged;
parent = Parent;
if (parent != null) parent.SizeChanged += onParentSizeChanged;
};
}
private void SetLocation(Point location)
{
var rect = new Rectangle(location, Size);
var clipRect = Parent.DisplayRectangle;
if (rect.Right > clipRect.Right) rect.X -= (rect.Right - clipRect.Right);
if (rect.X < clipRect.X) rect.X = clipRect.X;
if (rect.Bottom > clipRect.Bottom) rect.Y -= (rect.Bottom - clipRect.Bottom);
if (rect.Y < clipRect.Y) rect.Y = clipRect.Y;
location = rect.Location;
if (this.Location == location) return;
this.Location = location;
}
}
public enum Axis { X, Y, None }
// You haven't provided these, so I'm guessing by the usage
static class Utils
{
public static Point Add(this Point left, Point right)
{
return new Point(left.X + right.X, left.Y + right.Y);
}
public static Point Sub(this Point left, Point right)
{
return new Point(left.X - right.X, left.Y - right.Y);
}
public static Point Rasta(this Point pt, Axis axis, int value)
{
// Have absolutely no idea what is this about
return pt;
}
}
Related
I found on the internet how to create your own checkbox. Unfortunately, there is a problem with the fact that you have to click very slowly to react to a change, and when I click on the text, nothing changes. I presented the whole action in the film. Please help.
https://www.youtube.com/watch?v=s6xmVAoUVJ8
Here are the files: https://drive.google.com/file/d/10pj6DRjCvfQc8_s0rUufJpN1kdft2sml/view?usp=sharing
CheckBox.cs:
class NowyCheckbox : Control
{
#region Private members
public bool IsChecked = false;
private Label CheckBoxLabel;
private Rectangle CheckBoxRectangle;
private bool MouseOver = false;
#endregion
#region Public Members (in Attributes)
private Color CheckBoxCharColorValue = Color.FromArgb(0, 0, 0);
public Color CheckBoxCharColor
{
get
{
return CheckBoxCharColorValue;
}
set
{
if (CheckBoxCharColor != value)
{
CheckBoxCharColorValue = value;
Invalidate();
}
}
}
private Color CheckBoxCharHighlightColorValue = Color.FromArgb(0, 120, 215);
public Color CheckBoxCharHighlightColor
{
get
{
return CheckBoxCharHighlightColorValue;
}
set
{
if (CheckBoxCharHighlightColor != value)
{
CheckBoxCharHighlightColorValue = value;
Invalidate();
}
}
}
private Font CheckBoxCharFontValue;
public Font CheckBoxCharFont
{
get
{
return CheckBoxCharFontValue;
}
set
{
if (CheckBoxCharFont != value)
{
CheckBoxCharFontValue = value;
Invalidate();
}
}
}
private string CheckBoxCharValue = "!";
public string CheckBoxChar
{
get
{
return CheckBoxCharValue;
}
set
{
if (CheckBoxChar != value)
{
CheckBoxCharValue = value;
RefreshLabel();
}
}
}
private Font CheckBoxFontValue;
public Font CheckBoxFont
{
get
{
return CheckBoxFontValue;
}
set
{
if (CheckBoxFont != value)
{
CheckBoxFontValue = value;
RefreshLabel();
}
}
}
private string CheckBoxTextValue = "Nowy CheckBox";
public string CheckBoxText
{
get
{
return CheckBoxTextValue;
}
set
{
if (CheckBoxText != value)
{
CheckBoxTextValue = value;
RefreshLabel();
}
}
}
private int CheckBoxSizeValue = 12;
public int CheckBoxSize
{
get
{
return CheckBoxSizeValue;
}
set
{
if (CheckBoxSize != value)
{
CheckBoxSizeValue = value;
RefreshLabel();
Invalidate();
}
}
}
private int CheckBoxFrameStrengthValue = 1;
public int CheckBoxFrameStrength
{
get
{
return CheckBoxFrameStrengthValue;
}
set
{
if (CheckBoxFrameStrengthValue != value)
{
CheckBoxFrameStrengthValue = value;
Invalidate();
}
}
}
#region Public Member
private int CheckBoxCharOffsetXValue = 0;
public int CheckBoxCharOffsetX
{
get
{
return CheckBoxCharOffsetXValue;
}
set
{
if (CheckBoxCharOffsetX != value)
{
CheckBoxCharOffsetXValue = value;
Invalidate();
}
}
}
private int CheckBoxCharOffsetYValue = 0;
public int CheckBoxCharOffsetY
{
get
{
return CheckBoxCharOffsetYValue;
}
set
{
if (CheckBoxCharOffsetY != value)
{
CheckBoxCharOffsetYValue = value;
Invalidate();
}
}
}
private int CheckBoxOffsetXValue = 0;
public int CheckBoxOffsetX
{
get
{
return CheckBoxOffsetXValue;
}
set
{
if (CheckBoxOffsetX != value)
{
CheckBoxOffsetXValue = value;
RefreshLabel();
}
}
}
private int CheckBoxOffsetYValue = 0;
public int CheckBoxOffsetY
{
get
{
return CheckBoxOffsetYValue;
}
set
{
if (CheckBoxOffsetY != value)
{
CheckBoxOffsetYValue = value;
RefreshLabel();
}
}
}
#endregion
#region Public Colors
private Color CheckBoxFrameColorValue = Color.FromArgb(0, 0, 0);
public Color CheckBoxFrameColor
{
get
{
return CheckBoxFrameColorValue;
}
set
{
if (CheckBoxFrameColorValue != value)
{
CheckBoxFrameColorValue = value;
Invalidate();
}
}
}
private Color CheckBoxFrameHighlightColorValue = Color.FromArgb(0, 120, 250);
public Color CheckBoxFrameHighlightColor
{
get
{
return CheckBoxFrameHighlightColorValue;
}
set
{
if (CheckBoxFrameHighlightColorValue != value)
{
CheckBoxFrameHighlightColorValue = value;
Invalidate();
}
}
}
private Color CheckBoxBackColorValue = Color.FromArgb(255, 255, 255);
public Color CheckBoxBackColor
{
get
{
return CheckBoxBackColorValue;
}
set
{
if (CheckBoxBackColorValue != value)
{
CheckBoxBackColorValue = value;
Invalidate();
}
}
}
private Color CheckBoxBackHighlightColorValue = Color.FromArgb(255, 255, 255);
public Color CheckBoxBackHighlightColor
{
get
{
return CheckBoxBackHighlightColorValue;
}
set
{
if (CheckBoxBackHighlightColorValue != value)
{
CheckBoxBackHighlightColorValue = value;
Invalidate();
}
}
}
private Color CheckBoxForeHighlightColorValue = Color.FromArgb(0, 0, 0);
public Color CheckBoxForeHighlightColor
{
get
{
return CheckBoxForeHighlightColorValue;
}
set
{
if (CheckBoxForeHighlightColorValue != value)
{
CheckBoxForeHighlightColorValue = value;
RefreshLabel();
}
}
}
private Color CheckBoxForeColorValue = Color.FromArgb(0, 0, 0);
public Color CheckBoxForeColor
{
get
{
return CheckBoxForeColorValue;
}
set
{
if (CheckBoxForeColorValue != value)
{
CheckBoxForeColorValue = value;
RefreshLabel();
}
}
}
#endregion
#endregion
#region Constructor
public NowyCheckbox()
{
DoubleBuffered = true;
Size = new Size(112, 18);
CheckBoxFont = this.Font;
CheckBoxCharFont = this.Font;
var midHeight = 8 - Font.Height / 2;
CheckBoxLabel = new Label()
{
Font = CheckBoxFont,
Size = new Size(this.Width - 16, this.Height),
Location = new Point(16, midHeight),
Text = CheckBoxText
};
Controls.Add(CheckBoxLabel);
CreateMouseEvents();
}
#endregion
#region Create mouse events
private void CreateMouseEvents()
{
MouseEnter += (sender, e) =>
{
OnCustomMouseEnter(e);
};
MouseLeave += (sender, e) =>
{
OnCustomMouseLeave(e);
};
MouseDown += (sender, e) =>
{
OnCustomMouseDown(e);
};
CheckBoxLabel.MouseEnter += (sender, e) =>
{
OnCustomMouseEnter(e);
};
CheckBoxLabel.MouseLeave += (sender, e) =>
{
OnCustomMouseLeave(e);
};
CheckBoxLabel.MouseDown += (sender, e) =>
{
OnCustomMouseDown(e);
};
}
#endregion
#region Mouse Events
private void OnCustomMouseDown(EventArgs e)
{
IsChecked = !IsChecked;
Invalidate();
}
private void OnCustomMouseEnter(EventArgs e)
{
if (MouseOver == false)
{
MouseOver = true;
Invalidate();
RefreshLabel();
}
}
private void OnCustomMouseLeave(EventArgs e)
{
if (MouseOver == true)
{
MouseOver = false;
Invalidate();
RefreshLabel();
}
}
#endregion
#region Paint NowyCheckbox
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
PaintRectangle(e);
if (IsChecked == true)
{
PaintArrowChar(e);
}
}
#endregion
#region Paint NowyCheckboxRectangle
private void PaintRectangle(PaintEventArgs e)
{
var midHeight = Height / 2 - CheckBoxSize / 2;
CheckBoxRectangle = new Rectangle(0, midHeight, CheckBoxSize, CheckBoxSize);
var fillColor = MouseOver == true ? CheckBoxBackHighlightColor : CheckBoxBackColor;
var frameColor = MouseOver == true ? CheckBoxFrameHighlightColor : CheckBoxFrameColor;
using (var pen = new Pen(frameColor, CheckBoxFrameStrength))
{
var brush = new SolidBrush(fillColor);
e.Graphics.FillRectangle(brush, CheckBoxRectangle);
e.Graphics.DrawRectangle(pen, CheckBoxRectangle);
}
}
#endregion
#region Paint Checkbox Arrow
private void PaintArrowChar(PaintEventArgs e)
{
var charColor = MouseOver == true ? CheckBoxCharHighlightColor : CheckBoxCharColor;
var midX = CheckBoxSize / 2 - 3 + CheckBoxCharOffsetX;
var midY = Height / 2 - CheckBoxCharFont.Height / 2 + CheckBoxCharOffsetY;
using (var brush = new SolidBrush(charColor))
{
e.Graphics.DrawString(CheckBoxChar, CheckBoxCharFont, brush, new Point(midX, midY));
}
}
#endregion
#region [OnResize]
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
RefreshLabel();
}
#endregion
#region Refresh Label
private void RefreshLabel()
{
if (CheckBoxLabel == null) return;
CheckBoxLabel.Font = CheckBoxFont;
CheckBoxLabel.Text = CheckBoxText;
CheckBoxLabel.ForeColor = MouseOver == true ? CheckBoxForeHighlightColor : CheckBoxForeColor;
var offsetWidth = Width - CheckBoxSize;
CheckBoxLabel.Size = new Size(offsetWidth, Height);
var offsetX = CheckBoxSize + 6 + CheckBoxOffsetX;
var midHeight = Height / 2 - CheckBoxFont.Height / 2 + CheckBoxOffsetY;
CheckBoxLabel.Location = new Point(offsetX, midHeight);
Invalidate();
}
#endregion
}
}
Form1.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Test
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void nowyCheckbox1_Click(object sender, EventArgs e)
{
if (nowyCheckbox1.IsChecked)
{
label2.Text = "Jestem włączony";
}
else
{
label2.Text = "Jestem wyłączony";
}
}
private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
if (checkBox1.Checked)
{
label3.Text = "Jestem włączony";
}
else
{
label3.Text = "Jestem wyłączony";
}
}
}
An object like your custom check box, should have a mechanism used to notify a subscriber (like a Form) that something has happened. Notifications like mouse clicks, keystrokes, property changes...etc. In event-driven programming, your control should generate and publish an event to be received and handled by a subscriber through an Event Handler. Your custom control is missing that mechanism and this is the problem.
Let's apply that and create a simple check box control.
[DefaultProperty("Checked")]
[DefaultEvent("CheckedChanged")]
[DesignerCategory("Code")]
[ToolboxBitmap(typeof(CheckBox))]
public class NowyCheckBox : Control
{
Class members to keep:
The bounds of the check box and text areas.
The points of a check mark.
The current mouse state.
private enum MouseState : int
{
None,
Over,
Down
}
private Rectangle ChkRec, TxtRec;
private PointF[] ChkPoints;
private MouseState mouseState = MouseState.None;
In the constructor, set the shown below styles to reduce the flickering and to use the control in a container with a background image or gradient colors.
public NowyCheckBox() : base()
{
SetStyle(
ControlStyles.SupportsTransparentBackColor |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.ResizeRedraw, true);
UpdateStyles();
}
Add a method to create the drawing objects. Call it from the setters of the relevant properties
(such as CheckBoxSize) and methods (like OnSizeChanged) as shown next.
private void ResetDrawingObjects()
{
if (Width == 0 || Height == 0) return;
ChkRec = new Rectangle(new Point(0, (Height - checkBoxSize.Height) / 2), CheckBoxSize);
TxtRec = new Rectangle(ChkRec.Right + 5, 0, Width - ChkRec.Right - 10, Height);
if (this.RightToLeft == RightToLeft.Yes)
{
ChkRec.X = Width - 1 - ChkRec.Width;
TxtRec.X = ChkRec.X - 5 - TxtRec.Width;
}
var r = Rectangle.Inflate(ChkRec, -3, -3);
ChkPoints = new[]
{
new PointF(r.X, r.Y + (r.Height / 2f)),
new PointF(r.X + (r.Width / 2f) - 1, r.Bottom),
new PointF(r.Right, r.Y)
};
Invalidate();
}
Create a custom event and raise it whenever the Checked property changes.
public event EventHandler CheckedChanged;
protected virtual void OnCheckedChanged(EventArgs e) => CheckedChanged?.Invoke(this, e);
Add the properties.
private bool _checked;
[DefaultValue(false)]
public bool Checked
{
get => _checked;
set
{
if (_checked != value)
{
_checked = value;
Invalidate();
OnCheckedChanged(new EventArgs());
}
}
}
private Size checkBoxSize = new Size(12, 12);
[DefaultValue(typeof(Size), "12, 12")]
public Size CheckBoxSize
{
get => checkBoxSize;
set { checkBoxSize = value; ResetDrawingObjects(); }
}
// Add the rest...
Override the following methods to:
Update the mouseState variable and refresh the drawing by calling the Invalidate method if needed.
Call the ResetDrawingObjects method whenever the drawing objects need to resize and/or reposition.
Like the default CheckBox control, toggle the Checked property on Keys.Space press.
Draw the control.
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
if (mouseState != MouseState.Over)
{
mouseState = MouseState.Over;
// Pass ChkRec to update the checkbox area only.
Invalidate(ChkRec);
}
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
if (e.Button == MouseButtons.Left)
{
Focus();
mouseState = MouseState.Down;
Invalidate(ChkRec);
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
Checked = !Checked;
mouseState = MouseState.Over;
Invalidate(ChkRec);
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
mouseState = MouseState.None;
Invalidate(ChkRec);
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.KeyCode == Keys.Space) Checked = !Checked;
}
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
Invalidate();
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
ResetDrawingObjects();
}
protected override void OnRightToLeftChanged(EventArgs e)
{
base.OnRightToLeftChanged(e);
ResetDrawingObjects();
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
var g = e.Graphics;
// Get your colors to fill and draw...
var clr = mouseState == MouseState.Down
? // down color...
: mouseState == MouseState.Over
? // over color...
: // default color...;
using (var pn = new Pen(clr)) g.DrawRectangle(pn, ChkRec);
if (Checked)
{
clr = mouseState == MouseState.Down
? // down color...
: mouseState == MouseState.Over
? // over color...
: // default color...;
using (var pn = new Pen(clr, 2))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawLines(pn, ChkPoints);
g.SmoothingMode = SmoothingMode.None;
}
/*
* In case you prefere drawing glyphs. Example:
using (var fnt = new Font("Marlett", 12))
{
var r = ChkRec;
r.Offset(2, -2);
TextRenderer.DrawText(g, "a", fnt, r, clr,
TextFormatFlags.HorizontalCenter |
TextFormatFlags.VerticalCenter);
}
*/
}
var flags = TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis;
if (this.RightToLeft == RightToLeft.Yes)
flags |= TextFormatFlags.Right;
else
flags |= TextFormatFlags.Left;
TextRenderer.DrawText(g, Text, Font, TxtRec, ForeColor, flags);
}
}
Put it together, rebuild, drop an instance, double click on it to subscribe to the default event CheckedChanged, and handle it as you want.
private void nowyCheckBox1_CheckedChanged(object sender, EventArgs e)
{
label2.Text = nowyCheckBox1.Checked
? "Jestem włączony"
: "Jestem wyłączony";
}
If you'd like to read more.
Handle and raise events
C# - Events
I have found the following two label controls that separately handle marquee scrolling of text in a label, and also for the label to be partly transparent. They work very well separately, but I'm having trouble combining them into one control given my limited C#.
Can anyone give me some clues?
Transparent label:
public class LabelTransparent : Label
{
private int opacity;
public Color clrTransparentColor;
public LabelTransparent()
{
this.clrTransparentColor = Color.Blue;
this.opacity = 50;
this.BackColor = Color.Transparent;
}
protected override void OnPaint(PaintEventArgs e)
{
if (Parent != null)
{
using (var bmp = new Bitmap(Parent.Width, Parent.Height))
{
Parent.Controls.Cast<Control>()
.Where(c => Parent.Controls.GetChildIndex(c) > Parent.Controls.GetChildIndex(this))
.Where(c => c.Bounds.IntersectsWith(this.Bounds))
.OrderByDescending(c => Parent.Controls.GetChildIndex(c))
.ToList()
.ForEach(c => c.DrawToBitmap(bmp, c.Bounds));
e.Graphics.DrawImage(bmp, -Left, -Top);
using (var b = new SolidBrush(Color.FromArgb(this.Opacity, this.TransparentBackColor)))
{
e.Graphics.FillRectangle(b, this.ClientRectangle);
}
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
TextRenderer.DrawText(e.Graphics, this.Text, this.Font, this.ClientRectangle, this.ForeColor, Color.Transparent);
}
}
}
public int Opacity
{
get { return opacity; }
set { if (value >= 0 && value <= 255) opacity = value; this.Invalidate(); }
}
public Color TransparentBackColor
{
get { return clrTransparentColor; }
set { clrTransparentColor = value; this.Invalidate(); }
}
[Browsable(false)]
public override Color BackColor
{
get { return Color.Transparent; }
set { base.BackColor = Color.Transparent; }
}
}
Marquee scrolling:
public class LabelMarquee : Label
{
private int CurrentPosition { get; set; }
private Timer Timer = new Timer();
private Graphics grText;
private float fTextPixels;
public int ScrollSpeed
{
get { return this.Timer.Interval; }
set { try { this.Timer.Interval = value; } catch (Exception) { this.Timer.Interval = 15; } }
}
public LabelMarquee()
{
UseCompatibleTextRendering = true;
grText = this.CreateGraphics();
Timer.Tick += new EventHandler(Timer_Tick);
Timer.Start();
}
void Timer_Tick(object sender, EventArgs e)
{
fTextPixels = grText.MeasureString(this.Text, this.Font).Width;
if (CurrentPosition < -this.fTextPixels) CurrentPosition = Width;
else CurrentPosition -= 1;
Invalidate();
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.TranslateTransform((float)CurrentPosition, 0);
base.OnPaint(e);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (Timer != null)
Timer.Dispose();
}
Timer = null;
}
}
I have a button created programatically
InfoBtn = _utilProvider.FloatingBtn("My Button"); //Returns Floating UIButton
InfoBtn.Frame = new CGRect((ScreenWidth / 2) - 22.5, ScreenHeight - 65, 55, 55);
View.Add(InfoBtn);
I want to add drag and drop capability to it programatically. The problem is, the events hooked up by Xamarin wont let me use Custom Event handlers such as UIEvent in this manner (directly converted from https://www.cocoanetics.com/2010/11/draggable-buttons-labels/ iOS example):
InfoBtn.TouchDragInside += (btn, uievent) =>
{
var button = (UIButton)btn;
var e = (UIEvent)uievent; //ERROR: Can not convert type 'System.EventArgs' to 'UIKit.UIEvent'
// get the touch
UITouch touch = (UITouch)e.TouchesForView(button).AnyObject;
// get delta
CGPoint previousLocation = touch.PreviousLocationInView(button);
CGPoint location = touch.LocationInView(button);
nfloat delta_x = location.X - previousLocation.X;
nfloat delta_y = location.Y - previousLocation.Y;
// move button
button.Center = new CGPoint(button.Center.X + delta_x, button.Center.Y + delta_y);
};
According to the example of Xamarin for Using Touch in iOS https://developer.xamarin.com/guides/ios/application_fundamentals/touch/ios_touch_walkthrough/ they use a Storyboard.
a custom UIViewController is created
partial class TouchViewController : UIViewController
and assigned to the ViewController by setting custom class
<viewController id="18" sceneMemberID="viewController" customClass="TouchViewController">
How can that customClass be set programatically?
I have also tried adding a UIGestureRecognizer
InfoBtn.AddGestureRecognizer(new MyGestureRecognizer());
partial class MyGestureRecognizer : UIGestureRecognizer
{
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
UITouch touch = touches.AnyObject as UITouch;
if (touch != null)
{
// Touch started
}
}
public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
base.TouchesCancelled(touches, evt);
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
base.TouchesEnded(touches, evt);
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
UITouch touch = touches.AnyObject as UITouch;
if (touch != null)
{
// move the shape
}
}
}
But it only ever enters TouchesBegan method.
I am frustrated by now after trying basically every tutorial found online.
Any help would be much appreciated
Okay, I got it to work using https://github.com/TwoRedCells/UIDragDropGestureRecognizer-Xamarin.iOS
I have modified the class a bit, here is the working code:
in ViewDidLoad():
InfoBtn = _utilProvider.FloatingBtn("My Button"); //Returns Floating UIButton
var dd = new DragDropGestureRecognizer();
dd.Dragging += (object sender, DragDropEventArgs e) =>
{
var view = ((DragDropGestureRecognizer)sender).View;
// Reposition box.
var x = e.ViewWasAt.X + e.Delta.X;
var y = e.ViewWasAt.Y + e.Delta.Y;
view.Center = new CGPoint(x, y);
};
InfoBtn.AddGestureRecognizer(dd);
InfoBtn.TouchUpInside += async (object sender, EventArgs e) =>
{
//Button On Click
};
InfoBtn.Frame = new CGRect((ScreenWidth / 2) - 22.5, ScreenHeight - 65, 55, 55);
View.Add(InfoBtn);
Added a class DragDropGestureRecognizer.cs with the same namespace as my application:
using System;
using CGPoint = System.Drawing.PointF;
using Foundation;
using UIKit;
namespace myNS
{
public class DragDropGestureRecognizer : UIGestureRecognizer
{
public DragDropGestureRecognizer()
{
}
public event EventHandler<DragDropEventArgs> Held;
protected void OnHeld(object sender, DragDropEventArgs e)
{
if (Held != null)
Held(sender, e);
}
public event EventHandler<DragDropEventArgs> Dragging;
protected void OnDragging(object sender, DragDropEventArgs e)
{
if (Dragging != null)
Dragging(sender, e);
}
public event EventHandler<DragDropEventArgs> Dropped;
protected void OnDropped(object sender, DragDropEventArgs e)
{
if (Dropped != null)
Dropped(sender, e);
}
public bool DidDrag { get; private set; }
public CGPoint DownAt { get; private set; }
public CGPoint DragAt { get; private set; }
public CGPoint ViewWasAt { get; private set; }
public CGPoint Delta
{
get { return new CGPoint(DragAt.X - DownAt.X, DragAt.Y - DownAt.Y); }
}
public bool Active { get { return DidDrag; } }
public override UIGestureRecognizerState State
{
get { return base.State; }
set { base.State = value; }
}
private CGPoint TouchPoint { get { return (CGPoint)LocationInView(View.Superview); } }
public override bool CanBePreventedByGestureRecognizer(UIGestureRecognizer preventingGestureRecognizer)
{
return false;
}
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
if (NumberOfTouches > 1)
{
State = UIGestureRecognizerState.Failed;
return;
}
OnHeld(this, new DragDropEventArgs(default(UIGestureRecognizerState), DragAt, Delta, ViewWasAt));
DownAt = TouchPoint;
ViewWasAt = (CGPoint)View.Center;
State = UIGestureRecognizerState.Possible;
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
base.TouchesEnded(touches, evt);
if (DidDrag)
{
State = UIGestureRecognizerState.Recognized;
OnDropped(this, new DragDropEventArgs(State, DragAt, Delta, ViewWasAt));
}
else
State = UIGestureRecognizerState.Failed;
}
public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
base.TouchesCancelled(touches, evt);
State = UIGestureRecognizerState.Failed;
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
if (State == UIGestureRecognizerState.Failed)
return;
DragAt = TouchPoint;
if (Distance(DownAt, DragAt) > 30 || DidDrag) //Won't move until dragged further than 30px
{
DidDrag = true;
OnDragging(this, new DragDropEventArgs(State, DragAt, Delta, ViewWasAt));
State = UIGestureRecognizerState.Changed;
}
}
public override void Reset()
{
base.Reset();
State = UIGestureRecognizerState.Possible;
DownAt = CGPoint.Empty;
DragAt = CGPoint.Empty;
DidDrag = false;
}
private float Distance(CGPoint point1, CGPoint point2)
{
var dx = point1.X - point2.X;
var dy = point1.Y - point2.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
}
}
public class DragDropEventArgs : EventArgs
{
public DragDropEventArgs(UIGestureRecognizerState state, CGPoint point, CGPoint delta, CGPoint viewWasAt)
{
State = state;
Point = point;
Delta = delta;
ViewWasAt = viewWasAt;
}
public UIGestureRecognizerState State { get; private set; }
public CGPoint Point { get; private set; }
public CGPoint Delta { get; private set; }
public CGPoint ViewWasAt { get; private set; }
}
}
Thanks to Yvan Rodrigues - TwoRedCells
Android Equivalent:
private int _xPad, _yPad, _xDelta, _yDelta;
prviate bool _moved;
InfoBtn.Touch += (v, me) => //InfoBtn is a button within a frameLayout
{
int X = (int)me.Event.RawX;
int Y = (int)me.Event.RawY;
switch (me.Event.Action & MotionEventActions.Mask)
{
case MotionEventActions.Down:
_xPad = frameLayout.PaddingLeft;
_yPad = frameLayout.PaddingTop;
_xDelta = X;
_yDelta = Y;
_moved = false;
break;
case MotionEventActions.Up:
if (!_moved)
{
//On Button Click
}
break;
case MotionEventActions.PointerDown:
break;
case MotionEventActions.PointerUp:
break;
case MotionEventActions.Move:
var _x = X - _xDelta;
var _y = Y - _yDelta;
_moved = _moved || Math.Abs(_x) > 100 || Math.Abs(_y) > 100; //100px
if (_moved)
{
var padleft = _x - _xPad;
padleft = padleft + InfoBtn.Width > Resources.DisplayMetrics.WidthPixels ? Resources.DisplayMetrics.WidthPixels - InfoBtn.Width : padleft;
var padtop = _y - _yPad;
padtop = padtop + InfoBtn.Height > Resources.DisplayMetrics.HeightPixels ? Resources.DisplayMetrics.HeightPixels - InfoBtn.Height : padtop;
frameLayout.SetPadding(0, 0, padleft, padtop);
}
break;
}
frameLayout.Invalidate();
};
I'm adding undo/redo functionality to my graphical editor, but I'm having some issues with undo/redo for move and resize.
The first thing is when I move my Control (Ellipse for example) and I click the undo button the Ellipse goes to another position than were it should go. When I press redo after the undo the Ellipse goes to the correct position.
Here is a link which will make it more clear what I mean: http://gyazo.com/d299fe5e52f08742e7fa2132f6ff9839
The second things which has a lot of agreements with the first thing is when I resize the Control (Ellipse for example) and I click undo the Ellipse has another size than it should have. After clicking redo again it has the right size again.
Here is a link which will make it more clear what I mean: http://gyazo.com/59ecdcba751ff3b8ffd053dd19cd9945
Maybe the problem is in the MoveCommand and ResizeCommand classes, my code is based on this code http://www.codeproject.com/Articles/33384/Multilevel-Undo-and-Redo-Implementation-in-Cshar. In the linked page they use WPF and Thickness is used for moving/resizing. In WinForms there's no Thickness but there's Margin/Padding which I also tried but with no luck. How to make the resizing / moving undo/redo work?
MoveCommand.cs
class MoveCommand : ICommand
{
Control _control;
int _changeOfTop;
int _changeOfLeft;
public MoveCommand(int top, int left, Control control)
{
_control = control;
_changeOfTop = top;
_changeOfLeft = left;
}
public void Execute()
{
_control.Top = _control.Top + _changeOfTop;
_control.Left = _control.Left + _changeOfLeft;
}
public void UnExecute()
{
_control.Top = _control.Top - _changeOfTop;
_control.Left = _control.Left - _changeOfLeft;
}
}
ResizeCommand.cs
class ResizeCommand : ICommand
{
private int _changeOfWidth;
private int _changeOfHeight;
private Control _control;
public ResizeCommand(int width, int height, Control control)
{
_changeOfWidth = width;
_changeOfHeight = height;
_control = control;
}
public void Execute()
{
_control.Height = _control.Height + _changeOfHeight;
_control.Width = _control.Width + _changeOfWidth;
}
public void UnExecute()
{
_control.Height = _control.Height - _changeOfHeight;
_control.Width = _control.Width - _changeOfWidth;
}
}
Ellipse.cs
class Ellipse : Control
{
private Point mDown { get; set; }
private Point conLoc { get; set; }
private bool userResizing = false;
private Form1 form =null;
public Ellipse(Form1 form1)
{
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
this.BackColor = Color.Transparent;
this.DoubleBuffered = true;
this.ResizeRedraw = true;
this.form = form1;
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
// Draw a black ellipse in the rectangle represented by the control.
e.Graphics.FillEllipse(Brushes.Black, 0, 0, Width, Height);
}
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
mDown = e.Location;
conLoc = this.Location;
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
if (this.Location != conLoc)
{
Console.WriteLine("You moved me");
this.form.InsertInUnDoRedoForMove(conLoc.Y, conLoc.X, this);
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
// Call MyBase.OnMouseMove to activate the delegate.
base.OnMouseMove(e);
if (e.Button == MouseButtons.Left)
{
Location = new Point(e.X + Left - mDown.X, e.Y + Top - mDown.Y);
}
}
/* Allow resizing at the bottom right corner */
protected override void WndProc(ref Message m)
{
const int wmNcHitTest = 0x84;
const int htBottomLeft = 16;
const int htBottomRight = 17;
const int WM_EXITSIZEMOVE = 0x232;
const int WM_NCLBUTTONDWN = 0xA1;
if (m.Msg == WM_NCLBUTTONDWN)
{
if (!userResizing)
{
userResizing = true;
Console.WriteLine("Start Resizing");
this.form.InsertInUnDoRedoForResize(this.Width, this.Height, this);
}
}
else if (m.Msg == WM_EXITSIZEMOVE)
{
if (userResizing)
{
userResizing = false;
Console.WriteLine("Finish Resizing");
}
}
else if (m.Msg == wmNcHitTest)
{
int x = (int)(m.LParam.ToInt64() & 0xFFFF);
int y = (int)((m.LParam.ToInt64() & 0xFFFF0000) >> 16);
Point pt = PointToClient(new Point(x, y));
Size clientSize = ClientSize;
if (pt.X >= clientSize.Width - 16 &&
pt.Y >= clientSize.Height - 16 &&
clientSize.Height >= 16)
{
m.Result = (IntPtr)(IsMirrored ? htBottomLeft : htBottomRight);
return;
}
}
base.WndProc(ref m);
}
}
Form1.cs
public partial class Form1 : Form
{
private bool draw;
private int x, y, xe, ye;
private Stack<ICommand> _Undocommands = new Stack<ICommand>();
private Stack<ICommand> _Redocommands = new Stack<ICommand>();
public Form1()
{
InitializeComponent();
menuComboBoxShape.ComboBox.DataSource = Enum.GetValues(typeof(Item));
}
public enum Item
{
Pencil,
Rectangle,
Ellipse,
}
private void panel_MouseDown(object sender, MouseEventArgs e)
{
draw = true;
x = e.X;
y = e.Y;
}
private void panel_MouseUp(object sender, MouseEventArgs e)
{
draw = false;
xe = e.X;
ye = e.Y;
Item item;
Enum.TryParse<Item>(menuComboBoxShape.ComboBox.SelectedValue.ToString(), out item);
switch (item)
{
case Item.Pencil:
using (Graphics g = panel.CreateGraphics())
using (var pen = new Pen(System.Drawing.Color.Black)) //Create the pen used to draw the line (using statement makes sure the pen is disposed)
{
g.DrawLine(pen, new Point(x, y), new Point(xe, ye));
}
break;
case Item.Rectangle:
var box = new Box(this);
panel.Controls.Add(box);
box.Location = new Point(x, y);
box.Width = (xe - x);
box.Height = (ye - y);
//InsertCommand boxcmd = new InsertCommand(box, panel);
InsertInUnDoRedoForInsert(box);
break;
case Item.Ellipse:
var el = new Ellipse(this);
panel.Controls.Add(el);
el.Location = new Point(x, y);
el.Width = (xe - x);
el.Height = (ye - y);
//InsertCommand elcmd = new InsertCommand(el,panel);
InsertInUnDoRedoForInsert(el);
break;
default:
break;
}
}
private void undoButton_Click(object sender, EventArgs e)
{
Undo(1);
}
private void redoButton_Click(object sender, EventArgs e)
{
Redo(1);
}
private void clearAllButton_Click(object sender, EventArgs e)
{
commandList.Clear();
current = 0;
panel.Controls.Clear();
}
public void Redo(int levels)
{
for (int i = 1; i <= levels; i++)
{
if (_Redocommands.Count != 0)
{
ICommand command = _Redocommands.Pop();
command.Execute();
_Undocommands.Push(command);
}
Invalidate();
}
}
public void Undo(int levels)
{
for (int i = 1; i <= levels; i++)
{
if (_Undocommands.Count != 0)
{
ICommand command = _Undocommands.Pop();
command.UnExecute();
_Redocommands.Push(command);
}
Invalidate();
}
}
#region UndoHelperFunctions
public void InsertInUnDoRedoForInsert(Control control)
{
ICommand cmd = new InsertCommand(control, panel,shapeTreeView);
_Undocommands.Push(cmd); _Redocommands.Clear();
}
public void InsertInUnDoRedoForResize
(int width, int height, Control control)
{
ICommand cmd = new ResizeCommand(width, height, control);
_Undocommands.Push(cmd); _Redocommands.Clear();
}
public void InsertInUnDoRedoForMove
(int top, int left, Control control)
{
ICommand cmd = new MoveCommand(top,left,control);
_Undocommands.Push(cmd); _Redocommands.Clear();
}
#endregion
}
I am trying to set a tooltip on an arbitrary range of text on a richtextbox. Is this possible? If so how would I do it e.g by passing in parameters "from" and "to" as (int) indexes.
Thanks
You could use the following as a starting point:
Add a reference to System.Windows.Interactivity.
Add the following classes to your project:
public class TextRangeToolTip
{
public int StartPosition { get; set; }
public int Length { get; set; }
public object ToolTip { get; set; }
internal bool IsInRange(int position)
{
return this.StartPosition <= position && position < this.StartPosition + this.Length;
}
}
public class TextRangeToolTipCollection : ObservableCollection<TextRangeToolTip> {}
[ContentProperty("Ranges")]
public class ToolTipBehavior : Behavior<RichTextBox>
{
private const int ToolTipHideMouseDelta = 9;
public static readonly DependencyProperty RangesProperty
= DependencyProperty.Register("Ranges", typeof(TextRangeToolTipCollection),
typeof (ToolTipBehavior),
new PropertyMetadata(OnRangesChanged));
private readonly DispatcherTimer timer;
private readonly ToolTip toolTip;
private Point lastMousePosition;
public TextRangeToolTipCollection Ranges
{
get
{
return (TextRangeToolTipCollection)this.GetValue(RangesProperty)
?? (this.Ranges = new TextRangeToolTipCollection());
}
set { this.SetValue(RangesProperty, value); }
}
public ToolTipBehavior()
{
this.Ranges = new TextRangeToolTipCollection();
this.timer = new DispatcherTimer();
this.timer.Tick += this.TimerOnTick;
this.timer.Interval = TimeSpan.FromSeconds(1);
this.toolTip = new ToolTip {Placement = PlacementMode.Relative};
}
protected override void OnAttached()
{
this.AssociatedObject.ToolTip = this.toolTip;
this.toolTip.PlacementTarget = this.AssociatedObject;
ToolTipService.SetIsEnabled(this.AssociatedObject, false);
this.AssociatedObject.MouseMove += this.AssociatedObjectOnMouseMove;
}
protected override void OnDetaching()
{
this.timer.Stop();
this.toolTip.PlacementTarget = null;
this.AssociatedObject.ToolTip = null;
this.AssociatedObject.ClearValue(ToolTipService.IsEnabledProperty);
this.AssociatedObject.MouseMove -= this.AssociatedObjectOnMouseMove;
}
private void AssociatedObjectOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
Point currentMousePosition = mouseEventArgs.GetPosition(this.AssociatedObject);
if (this.AssociatedObject.IsMouseCaptured)
{
Vector delta = currentMousePosition
- this.lastMousePosition;
if (delta.X*delta.X + delta.Y*delta.Y <= ToolTipHideMouseDelta)
{
this.toolTip.HorizontalOffset = currentMousePosition.X + 10;
this.toolTip.VerticalOffset = currentMousePosition.Y + 10;
return;
}
this.AssociatedObject.ReleaseMouseCapture();
this.toolTip.IsOpen = false;
}
if (this.AssociatedObject.IsMouseOver)
{
this.lastMousePosition = currentMousePosition;
this.timer.Stop();
this.timer.Start();
}
}
private static void OnRangesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ToolTipBehavior) d).OnRangesChanged((IEnumerable<TextRangeToolTip>) e.OldValue,
(IEnumerable<TextRangeToolTip>) e.NewValue);
}
private void OnRangesChanged(IEnumerable<TextRangeToolTip> oldRanges, IEnumerable<TextRangeToolTip> newRanges)
{
var oldObservable = oldRanges as INotifyCollectionChanged;
if (oldObservable != null)
{
CollectionChangedEventManager.RemoveHandler(oldObservable, this.OnRangesCollectionChanged);
}
var newObservable = newRanges as INotifyCollectionChanged;
if (newObservable != null)
{
CollectionChangedEventManager.AddHandler(newObservable, this.OnRangesCollectionChanged);
}
this.UpdateToolTip();
}
private void OnRangesCollectionChanged(
object sender,
NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
{
this.UpdateToolTip();
}
private bool SetToolTipData()
{
if (this.Ranges == null)
{
return false;
}
TextPointer pointer = this.AssociatedObject.GetPositionFromPoint(this.lastMousePosition, false);
if (pointer == null)
{
return false;
}
int position = this.AssociatedObject.Document.ContentStart.GetOffsetToPosition(pointer);
TextRangeToolTip matchingRange = this.Ranges.FirstOrDefault(r => r.IsInRange(position));
if (matchingRange == null)
{
return false;
}
this.toolTip.Content = matchingRange.ToolTip;
return true;
}
private void TimerOnTick(object sender, EventArgs eventArgs)
{
this.timer.Stop();
if (this.AssociatedObject.IsMouseOver && this.SetToolTipData())
{
this.toolTip.IsOpen = true;
this.AssociatedObject.CaptureMouse();
}
}
private void UpdateToolTip()
{
if (this.AssociatedObject != null && this.AssociatedObject.IsMouseCaptured && !this.SetToolTipData())
{
this.toolTip.IsOpen = false;
this.AssociatedObject.ReleaseMouseCapture();
}
}
}
Use it on your RichTextBox like this:
<RichTextBox>
<i:Interaction.Behaviors>
<myapp:ToolTipBehavior>
<myapp:TextRangeToolTip StartPosition="10" Length="4" ToolTip="some" />
<myapp:TextRangeToolTip StartPosition="15" Length="4" ToolTip="text" />
</myapp:ToolTipBehavior>
</i:Interaction.Behaviors>
<FlowDocument>
<Paragraph>This is some text. This is some other text.</Paragraph>
</FlowDocument>
</RichTextBox>
Alternatively, you can bind a TextRangeToolTipCollection to the Ranges property like this:
<RichTextBox Document="{Binding Document}">
<i:Interaction.Behaviors>
<myapp:ToolTipBehavior Ranges="{Binding RangeToolTips}" />
</i:Interaction.Behaviors>
</RichTextBox>
Getting the positions right is a bit tricky, because WPF counts symbols, not characters. You could extend the TextRangeToolTip class to have properties of type TextPointer or TextRange and construct it using your FlowDocument instance.