locked
How to add tooltip to rectangle in adorner RRS feed

  • Question

  • I have a horizontal slider representing a time interval. I want to mark some times with narrow vertical rectangles (or vertical  lines) with tooltips providing descriptions. An adorner seems reasonable to create the rectangles, doing so in the OnRender override for the adorner. How can I connect tooltips to the rectangles?

    Bill Swartz

    Friday, May 3, 2013 2:51 PM

Answers

  • I see. I wrote a ControlTemplate to accomplish this with a slider.  The positions of the tips are determined by binding a value to the Margin property of the visual.  I wouldn't control the position in this way typically.  It was just an easy way to throw it down.  I would probably write a ViewModel to calculate the position based on the ActualWidth of the slider, and the time value that you were talking about.  Anyway, here is the code:

        <Window.Resources>
            <local:DataRepository x:Key="Data"/>
            
            <DataTemplate 
                DataType="{x:Type local:FauxData}">
                <Rectangle
                    Margin="{Binding Path=DataMargin,Mode=OneWay}"
                    Fill="Gray"
                    Width="5"
                    Height="15">
                    <Rectangle.ToolTip>
                        <ToolTip
                            Content="{Binding Path=Descriptor,Mode=OneWay}"/>
                    </Rectangle.ToolTip>
                </Rectangle>
            </DataTemplate>
            
            <ControlTemplate
                x:Key="SliderControlTemplate"
                TargetType="{x:Type Slider}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="20"/>
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    
                    <ItemsControl
                        Name="ItemsSourceData"
                        ItemsSource="{Binding Source={StaticResource Data},Path=DataCollection}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <Grid/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>
    
                    <Border
                        Grid.Row="1"
                        Height="4"
                        Background="WhiteSmoke"
                        BorderBrush="Gray"
                        BorderThickness="0.5"
                        SnapsToDevicePixels="True"/>
                    
                    <Track
                        Grid.Row="1"
                        Name="PART_Track">
                        <Track.Thumb>
                            <Thumb
                                SnapsToDevicePixels="False"
                                Height="15"
                                Width="5"/>
                        </Track.Thumb>
                    </Track>
                </Grid>
            </ControlTemplate>
            
        </Window.Resources>
        
        <DockPanel
            VerticalAlignment="Center">
            <Slider 
                DockPanel.Dock="Top"
                Margin="10"
                Template="{StaticResource SliderControlTemplate}">
                
            </Slider>
            
        </DockPanel>
    </Window>

    And the data class and data repository with the fake data that somewhat matches your model:

    using System.ComponentModel;
    using System.Windows;
    
    namespace SliderWithToolTipsExample
    {
        public class FauxData : INotifyPropertyChanged
        {
            double _time;
            string _descriptor;
            Thickness _dataMargin;
    
            public FauxData(double time, string descriptor)
            {
                Time = time;
                Descriptor = descriptor;
                _dataMargin = new Thickness(time,0,0,0);
            }
    
            public Thickness DataMargin 
            {
                get { return _dataMargin; }
            }
    
            public double Time
            {
                get { return _time; }
                set
                {
                    if (value == _time)
                        return;
    
                    _time = value;
                    OnPropertyChanged("Time");
                }
            }
    
            public string Descriptor 
            {
                get { return _descriptor; }
                set
                {
                    if (value == _descriptor)
                        return;
    
                    _descriptor = value;
                    OnPropertyChanged("Descriptor");
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void OnPropertyChanged(string propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
    
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }

    using System.Collections.ObjectModel;
    using System.ComponentModel;
    
    namespace SliderWithToolTipsExample
    {
        public class DataRepository : INotifyPropertyChanged
        {
            ObservableCollection<FauxData> _collection;
    
            public DataRepository()
            {
                _collection = new ObservableCollection<FauxData>();
    
                _collection.Add(new FauxData(0.0, "First Event"));
                _collection.Add(new FauxData(36, "Second Event"));
                _collection.Add(new FauxData(72, "Third Event"));
                _collection.Add(new FauxData(100, "Last Event"));
            }
    
            public ObservableCollection<FauxData> DataCollection 
            {
                get { return _collection; }
                set
                {
                    if (value == _collection)
                        return;
    
                    _collection = value;
    
                    OnPropertyChanged("DataCollection");
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void OnPropertyChanged(string propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
    
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }


    • Edited by Matt Lynam Friday, May 3, 2013 9:13 PM
    • Proposed as answer by Matt Lynam Friday, May 3, 2013 9:14 PM
    • Marked as answer by Lisa Zhu Thursday, May 23, 2013 2:35 AM
    Friday, May 3, 2013 9:12 PM
  • I would definitely recommend a canvas for the ItemsPanelTemplate

                            <ItemsPanelTemplate>
                                <Canvas/>
                            </ItemsPanelTemplate>
    and then calculate the width in your VM.

    • Marked as answer by Lisa Zhu Thursday, May 23, 2013 2:35 AM
    Friday, May 3, 2013 9:57 PM

All replies

  • A standard Adorner implementation will not work well for what you are describing.  The DrawingContext that is used on the OnRender override is only capable of drawing visual shapes, not framework elements with their ToolTip property defined.  One example, found here, describes multiple Adorner style implementations including one where Adorner visuals are defined in an AttachedProperty and can be FrameworkElements.

    A better question in my mind is what are you trying to accomplish that isn't already included in the Slider class?  This Slider definition includes the built-in functionality of a basic slider and has a ToolTip which is displayed when sliding. 

            <Slider
                AutoToolTipPlacement="TopLeft"
                TickFrequency="2"
                TickPlacement="TopLeft"
                Ticks="1.,2.,3.,4.,5.,6.,7.,8.,9.,10."
                DockPanel.Dock="Bottom"/>

    Are you looking for the tick itself to present a ToolTip to the user when it is moused over?

    Friday, May 3, 2013 6:00 PM
  • The window containing the slider is bound to some data. The data contains a start time, an end time, and some events. The events are (time, text description) pairs. I want to indicate the times of the events on the slider with some kind of widget. I want the tooltip of each widget to be the corresponding text description.

    So the answer to your question is yes.


    Bill Swartz

    Friday, May 3, 2013 6:55 PM
  • I see. I wrote a ControlTemplate to accomplish this with a slider.  The positions of the tips are determined by binding a value to the Margin property of the visual.  I wouldn't control the position in this way typically.  It was just an easy way to throw it down.  I would probably write a ViewModel to calculate the position based on the ActualWidth of the slider, and the time value that you were talking about.  Anyway, here is the code:

        <Window.Resources>
            <local:DataRepository x:Key="Data"/>
            
            <DataTemplate 
                DataType="{x:Type local:FauxData}">
                <Rectangle
                    Margin="{Binding Path=DataMargin,Mode=OneWay}"
                    Fill="Gray"
                    Width="5"
                    Height="15">
                    <Rectangle.ToolTip>
                        <ToolTip
                            Content="{Binding Path=Descriptor,Mode=OneWay}"/>
                    </Rectangle.ToolTip>
                </Rectangle>
            </DataTemplate>
            
            <ControlTemplate
                x:Key="SliderControlTemplate"
                TargetType="{x:Type Slider}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="20"/>
                        <RowDefinition />
                    </Grid.RowDefinitions>
                    
                    <ItemsControl
                        Name="ItemsSourceData"
                        ItemsSource="{Binding Source={StaticResource Data},Path=DataCollection}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <Grid/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>
    
                    <Border
                        Grid.Row="1"
                        Height="4"
                        Background="WhiteSmoke"
                        BorderBrush="Gray"
                        BorderThickness="0.5"
                        SnapsToDevicePixels="True"/>
                    
                    <Track
                        Grid.Row="1"
                        Name="PART_Track">
                        <Track.Thumb>
                            <Thumb
                                SnapsToDevicePixels="False"
                                Height="15"
                                Width="5"/>
                        </Track.Thumb>
                    </Track>
                </Grid>
            </ControlTemplate>
            
        </Window.Resources>
        
        <DockPanel
            VerticalAlignment="Center">
            <Slider 
                DockPanel.Dock="Top"
                Margin="10"
                Template="{StaticResource SliderControlTemplate}">
                
            </Slider>
            
        </DockPanel>
    </Window>

    And the data class and data repository with the fake data that somewhat matches your model:

    using System.ComponentModel;
    using System.Windows;
    
    namespace SliderWithToolTipsExample
    {
        public class FauxData : INotifyPropertyChanged
        {
            double _time;
            string _descriptor;
            Thickness _dataMargin;
    
            public FauxData(double time, string descriptor)
            {
                Time = time;
                Descriptor = descriptor;
                _dataMargin = new Thickness(time,0,0,0);
            }
    
            public Thickness DataMargin 
            {
                get { return _dataMargin; }
            }
    
            public double Time
            {
                get { return _time; }
                set
                {
                    if (value == _time)
                        return;
    
                    _time = value;
                    OnPropertyChanged("Time");
                }
            }
    
            public string Descriptor 
            {
                get { return _descriptor; }
                set
                {
                    if (value == _descriptor)
                        return;
    
                    _descriptor = value;
                    OnPropertyChanged("Descriptor");
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void OnPropertyChanged(string propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
    
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }

    using System.Collections.ObjectModel;
    using System.ComponentModel;
    
    namespace SliderWithToolTipsExample
    {
        public class DataRepository : INotifyPropertyChanged
        {
            ObservableCollection<FauxData> _collection;
    
            public DataRepository()
            {
                _collection = new ObservableCollection<FauxData>();
    
                _collection.Add(new FauxData(0.0, "First Event"));
                _collection.Add(new FauxData(36, "Second Event"));
                _collection.Add(new FauxData(72, "Third Event"));
                _collection.Add(new FauxData(100, "Last Event"));
            }
    
            public ObservableCollection<FauxData> DataCollection 
            {
                get { return _collection; }
                set
                {
                    if (value == _collection)
                        return;
    
                    _collection = value;
    
                    OnPropertyChanged("DataCollection");
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            private void OnPropertyChanged(string propertyName)
            {
                PropertyChangedEventHandler handler = PropertyChanged;
    
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    }


    • Edited by Matt Lynam Friday, May 3, 2013 9:13 PM
    • Proposed as answer by Matt Lynam Friday, May 3, 2013 9:14 PM
    • Marked as answer by Lisa Zhu Thursday, May 23, 2013 2:35 AM
    Friday, May 3, 2013 9:12 PM
  • Thanks for the input. I am now running your code. The initial layout looks right. But it doesn't resize correctly. Maybe a MultiBinding that is also bound to the width will work. I am considering using a canvas immediately above or below the slider. And then positioning the rectangles in the SizeChanged handler.

    Bill Swartz

    Friday, May 3, 2013 9:53 PM
  • I would definitely recommend a canvas for the ItemsPanelTemplate

                            <ItemsPanelTemplate>
                                <Canvas/>
                            </ItemsPanelTemplate>
    and then calculate the width in your VM.

    • Marked as answer by Lisa Zhu Thursday, May 23, 2013 2:35 AM
    Friday, May 3, 2013 9:57 PM
  • Hi Bill,

    I provisionally marked Matt's reply as answer.

    Please  feel  free to unmark it if you think the information does not help.

    Regards,


    Lisa Zhu [MSFT]
    MSDN Community Support | Feedback to us
    Develop and promote your apps in Windows Store
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Thursday, May 23, 2013 2:36 AM