Je viens d’avoir la nécessité d’une chose inexistante (temporairement j’espère) dans Silverlight.
J’ai eu besoin d’un TextBox scrollable capable de scroller automatiquement lors de l’ajout de texte au TextBox.
Evidement il faut que cette fonctionnalité s’auto-désactive lorsque l’utilisateur change la position de la ScrollBar et se réactive lorsqu’il replace la ScrollBar à sa position maximale.
Exemple d’utilisation
On peut avoir l’utilité de ce type de control pour des logs, sur un client ou un serveur…
L’existant
Le TextBox de Silverlight permet l’utilisation des scroll bars en utilisant les propriétés suivantes dans le XAML :
- ScrollViewer.VerticalScrollBarVisibility
- ScrollViewer.HorizontalScrollBarVisibility
Cependant, on n’a strictement aucun moyen de gérer les événements sur ces mêmes scroll bars, ni même de connaitre leur valeur.
Le seul control qui donne accès a la gestion concrète des événements est le ScrollBar, mais la c’est le drame. Pour l’utiliser il faut tout recréer de zéro pour l’associer a un control.
L’alternative
J’ai donc crée un UserControl qui contient un ScrollViewer (scrollViewer) qui contient lui même un TextBox (txtRaw).
XAML:
Le fonctionnement est assez simple.
La propriété AutoScrollToCaret permet à l’utilisateur de spécifier s’il veut que la fonctionnalité d’auto scroll soit active ou non. Si c’est le cas, on va “écouter” les événements de mouvement de la souris sur le ScrollViewer mais aussi celui de changement de texte du TextBox.
Lorsqu’un de ces événements nous parvient, on vérifie différentes informations concernant la ScrollBar :
- Sa position par rapport à la dernière position connue. Si elle a changée et que l’OriginalSource est de type Rectangle, alors cela signifie que c’est l’utilisateur qui a déplacé la position de la ScrollBar. On appelle donc une méthode qui va déterminer si le Control doit continuer d’auto scroller ou non en fonction de la position de la ScrollBar (si la position est au max alors on scrollera sinon non)
- La position maximum. Si elle a changée (suite a une augmentation ou une diminution du contenu du TextBox) ET que l’auto scroll est actif alors on envoie la ScrollBar a sa position maximale.
La variable _hasToCaret est à true lorsque la propriété AutoScrollToCaret est à true et que l’utilisateur n’a pas déplacé la position de la ScrollBar. On se base sur l’état de ce booléen afin de savoir si on doit auto scroller ou non.
Code:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Shapes;
using Project.Common.Delegates; // Namespace comprenant une simple definition pour StringDelegate
namespace Project.Controls
{
public partial class CustomTextBox : UserControl
{
#region Variables
private double _lastVerticalOffset = 0;
private double _lastScrollableHeight = 0;
private bool _hasToCaret = false;
private bool _autoScrollToCaret = false;
#endregion
#region Properties
public bool AutoScrollToCaret
{
get { return _autoScrollToCaret; }
set
{
_autoScrollToCaret = value;
if (value == true) // On active nos listeners
{
_hasToCaret = true;
scrollViewer.MouseMove += new MouseEventHandler(scrollViewer_MouseMove);
txtRaw.TextChanged += new TextChangedEventHandler(txtRaw_TextChanged);
ScrollToCaret();
}
else // On désactive nos listeners
{
_hasToCaret = false;
scrollViewer.MouseMove -= new MouseEventHandler(scrollViewer_MouseMove);
txtRaw.TextChanged -= new TextChangedEventHandler(txtRaw_TextChanged);
}
}
}
#endregion
#region Constructor
public CustomTextBox()
{
InitializeComponent();
}
#endregion
#region UI Events
private void scrollViewer_MouseMove(object sender, MouseEventArgs e)
{
CheckScrollBar((e == null ? null : e.OriginalSource));
}
private void txtRaw_TextChanged(object sender, TextChangedEventArgs e)
{
scrollViewer.UpdateLayout();//Obligatoire sinon la taille de la scroll bar n’est pas rafraichie avant de passer dans la méthode [j’y ai passé un petit moment avant de comprendre ca !!!]
CheckScrollBar(null);
}
#endregion
#region Scroll Management
private void CheckScrollBar(object origin)
{
double scrollableHeight = scrollViewer.ScrollableHeight;
double verticalOffset = scrollViewer.VerticalOffset;
if (verticalOffset != _lastVerticalOffset || // Si la position a changée ou
scrollableHeight != _lastScrollableHeight) // si la position max a changée
{
if (origin != null && origin is Rectangle) // Si l’origine est un Rectangle, alors l’utilisateur a déplacé la ScrollBar
ScrollChangedByMouseMove();
else if (_hasToCaret == true) // Si ce n’est pas le cas et que l’on doit auto-scroller… YES WE CAN
ScrollToCaret();
_lastVerticalOffset = verticalOffset;
_lastScrollableHeight = scrollableHeight;
}
}
private void ScrollChangedByMouseMove()
{
if (AutoScrollToCaret == true)
{
if (scrollViewer.VerticalOffset != scrollViewer.ScrollableHeight) // Si la position de la ScrollBar n’est pas au max
{
_hasToCaret = false; // on désactive l’auto-scroll
}
else // sinon
{
_hasToCaret = true; // on l’active
}
}
}
private void ScrollToCaret()
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.ScrollableHeight);
}
#endregion
#region Add Text
private void addText(string str)
{
txtRaw.Text += str;
}
internal void AddText(string str)
{
txtRaw.Dispatcher.BeginInvoke(new StringDelegate(addText), str);
}
#endregion
}
}
Cette source parait toute conne maintenant qu’elle fonctionne, mais j’ai bien tourné en rond avant de m’en sortir !
Voila, si ca peut dépanner une ou deux personnes, ce sera déjà pas mal.