1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【WPF】ツールチップにURLリンクを埋め込み良い感じに表示する方法

Last updated at Posted at 2024-08-23

概要

WPFを使用して、ツールチップにURLリンクを埋め込む実装を行ったので、備忘録としてまとめました。
タイトルにはツールチップにURLリンクを埋め込む以外ににも色々と仕様があったので、下記にまとめています。

仕様

  • 画面にボタンが3つある
  • 各ボタンはマウスオーバーすると、文章 + URLリンクがツールチップのように表示される
  • ツールチップの表示場所は、ボタンの下部とする
  • ツールチップが開かれた状態から他のボタンをマウスオーバーすると、開いていたツールチップは閉じ、マウスオーバーしたボタンのツールチップが開く
  • ボタンやツールチップからカーソルが離れると、ツールチップが閉じられる
  • ツールチップに表示されるURLは、URLの文字をそのまま表示するのではなく、文章をクリックしたらリンク先に飛ぶようにする
    ※例えばツールチップの一文で「ここをクリック」と表示されており、この文章をクリックしたらリンク先に飛ぶような挙動
  • ツールチップの文章やURLは外部ファイルに定義されるので、バインディングして表示する

実装方法

WPFのTooltipにはURLリンクを貼れるような機能は直接は提供されていなかったので、Popupを使用しました。
また、PGにて以下の挙動で実装しました。

ボタンをマウスオーバーしたとき

  • マウスオーバーされたボタンのツールチップを開く
  • マウスオーバーされていないボタンのツールチップは閉じる

ボタンがマウスアウトしたとき

  • 下記の条件に紐づくツールチップを閉じるイベントを、0.2秒後に発火させる
    • ボタンもしくはそのボタンに紐づくツールチップがマウスオーバーされていない場合、そのボタンのツールチップを閉じる

ツールチップのマウスアウト時

  • マウスアウトしたツールチップを閉じる

上記の組み合わせにより、下記を実現

  • ボタンからツールチップにカーソルが移動する際に、ツールチップが消えない

    • ボタンとツールチップの間には隙間が少しあるが、カーソルが隙間を移動する間はツールチップが消えない。※UIの見やすさ的に、ボタンとツールチップの間に隙間が出来る状態とした。
  • 複数のボタンの上を素早くカーソル移動させても表示されるツールチップが重複せず、
    最後にマウスオーバーされたツールチップのみが表示される

  • ボタンまたはツールチップからマウスアウトすると、ツールチップが消える

コード

View(xaml)

<!-- ボタン1つ目 hogeボタン -->
<StackPanel>
    <Button
        x:Name="HogeButton"
        Width="100"
        Content="hogeボタン"
        HorizontalContentAlignment="Center"
        VerticalContentAlignment="Center"
        MouseEnter="Button_MouseEnter"
        MouseLeave="Button_MouseLeave"
        Command="{Binding HogeCommand}"/>
    <Popup
        x:Name="HogePopup"
        Width="285"
        MouseLeave="ButtonPopup_MouseLeave">
        <StackPanel>
            <Border
                Background="WhiteSmoke"
                BorderBrush="Black"
                BorderThickness="0.8">
                <TextBlock Margin="5" Background="WhiteSmoke">
                    <Run Text="{Binding HogeButtonToolTip.Title}" FontWeight="Bold"/>
                    <LineBreak/>
                    <Run Text="{Binding HogeButtonToolTip.Text}"/>
                    <LineBreak/>
                    <Hyperlink
                        NavigateUri="{Binding HogeButtonToolTip.Url}"
                        Click="Hyperlink_Click">
                        <TextBlock Text="{Binding HogeButtonToolTip.UrlText}"/>
                    </Hyperlink>
                </TextBlock>
            </Border>
        </StackPanel>
    </Popup>
</StackPanel>
<!-- ボタン2つ目 hugaボタン -->
<StackPanel>
    <Button
        x:Name="HugaButton"
        Width="100"
        Content="hugaボタン"
        HorizontalContentAlignment="Center"
        VerticalContentAlignment="Center"
        MouseEnter="Button_MouseEnter"
        MouseLeave="Button_MouseLeave"
        Command="{Binding FugaCommand}"/>
    <Popup
        x:Name="HugaPopup"
        Width="285"
        MouseLeave="ButtonPopup_MouseLeave">
        <StackPanel>
            <Border
                Background="WhiteSmoke"
                BorderBrush="Black"
                BorderThickness="0.8">
                <TextBlock Margin="5" Background="WhiteSmoke">
                    <Run Text="{Binding HugaButtonToolTip.Title}" FontWeight="Bold"/>
                    <LineBreak/>
                    <Run Text="{Binding HugaButtonToolTip.Text}"/>
                    <LineBreak/>
                    <Hyperlink
                        NavigateUri="{Binding HugaButtonToolTip.Url}"
                        Click="Hyperlink_Click">
                        <TextBlock Text="{Binding HugaButtonToolTip.UrlText}"/>
                    </Hyperlink>
                </TextBlock>
            </Border>
        </StackPanel>
    </Popup>
</StackPanel>
<!-- ボタン3つ目 piyoボタン -->
<StackPanel>
    <Button
        x:Name="PiyoButton"
        Width="100"
        Content="piyoボタン"
        HorizontalContentAlignment="Center"
        VerticalContentAlignment="Center"
        MouseEnter="Button_MouseEnter"
        MouseLeave="Button_MouseLeave"
        Command="{Binding PiyoCommand}"/>
    <Popup
        x:Name="PiyoPopup"
        Width="285"
        MouseLeave="ButtonPopup_MouseLeave">
        <StackPanel>
            <Border
                Background="WhiteSmoke"
                BorderBrush="Black"
                BorderThickness="0.8">
                <TextBlock Margin="5" Background="WhiteSmoke">
                    <Run Text="{Binding PiyoButtonToolTip.Title}" FontWeight="Bold"/>
                    <LineBreak/>
                    <Run Text="{Binding PiyoButtonToolTip.Text}"/>
                    <LineBreak/>
                    <Hyperlink
                        NavigateUri="{Binding PiyoButtonToolTip.Url}"
                        Click="Hyperlink_Click">
                        <TextBlock Text="{Binding PiyoButtonToolTip.UrlText}"/>
                    </Hyperlink>
                </TextBlock>
            </Border>
        </StackPanel>
    </Popup>
</StackPanel>

View(コードビハインド)

       /// <summary>
        /// ボタン MouseEnterイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Button_MouseEnter(object sender, MouseEventArgs e)
            => HandleButtonTooltipOnMouseOver((Button)sender);

        /// <summary>
        /// ボタン MouseLeaveイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Button_MouseLeave(object sender, MouseEventArgs e)
        {
            var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(0.2) };
            timer.Tick += ClosePopupsOnTimerTick;
            timer.Start();
        }

        /// <summary>
        /// ボタンのPopup MouseLeaveイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ButtonPopup_MouseLeave(object sender, MouseEventArgs e)
            => ClosePopup((Popup)sender);

        /// <summary>
        /// Hyperlink Clickイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Hyperlink_Click(object sender, RoutedEventArgs e)
        {
            var link = (Hyperlink)sender;
            Process.Start(link.NavigateUri.ToString());
        }

        /// <summary>
        ///  マウスオーバーされたボタンのツールチップを表示し、
        ///  それ以外のボタンのツールチップを非表示にする
        /// </summary>
        /// <param name="button">マウスオーバーされたボタンのオブジェクト</param>
        private void HandleButtonTooltipOnMouseOver(Button button)
        {
            switch (button.Name)
            {
                case "HogeButton":
                    HogePopup.IsOpen = true;
                    HugaPopup.IsOpen = false;
                    PiyoPopup.IsOpen = false;
                    break;

                case "hugaButton":
                    HogePopup.IsOpen = false;
                    HugaPopup.IsOpen = true;
                    PiyoPopup.IsOpen = false;
                    break;

                case "piyoButton":
                    HogePopup.IsOpen = false;
                    HugaPopup.IsOpen = false;
                    PiyoPopup.IsOpen = true;
                    break;

                default:
                    // エラー処理
                    break;
            }
        }

        /// <summary>
        /// ボタンのポップアップを閉じるタイマーイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ClosePopupsOnTimerTick(object sender, EventArgs e)
        {
            // ボタンもしくはそのボタンに紐づくツールチップがマウスオーバーされていない場合、
            // そのボタンのツールチップを閉じる
            if (!HogeButton.IsMouseOver && !HogePopup.IsMouseOver) HogePopup.IsOpen = false;
            if (!HugaButton.IsMouseOver && !HugaPopup.IsMouseOver) HugaPopup.IsOpen = false;
            if (!PiyoButton.IsMouseOver && !PiyoPopup.IsMouseOver) PiyoPopup.IsOpen = false;

            var timer = (DispatcherTimer)sender;
            timer.Stop();
        }

        /// <summary>
        /// ツールチップを閉じる
        /// </summary>
        /// <param name="popup">ツールチップのオブジェクト</param>
        private void ClosePopup(Popup popup)
        {
            var panel = (StackPanel)popup.Parent;
            foreach (var child in panel.Children)
            {
                var button = child as Button;
                if (button == null) continue;
                switch (button.Name)
                {
                    case "HogeButton":
                        HogePopup.IsOpen = false;
                        break;

                    case "HugaButton":
                        HugaPopup.IsOpen = false;
                        break;

                    case "PiyoButton":
                        PiyoPopup.IsOpen = false;
                        break;

                    default:
                        // エラー処理 
                        break;
                }
            }
        }

ViewModel


        /// <summary>
        /// hogeボタンのボタンガイド情報
        /// </summary>
        public ButtonTooltip HogeButtonToolTip { get; private set; } = new ButtonTooltip();

        /// <summary>
        /// fugaボタンのボタンガイド情報
        /// </summary>
        public ButtonTooltip FugaButtonToolTip { get; private set; } = new ButtonTooltip();

        /// <summary>
        /// piyoのボタンガイド情報
        /// </summary>
        public ButtonTooltip PiyoButtonToolTip { get; private set; } = new ButtonTooltip();

        /// <summary>
        /// hogeボタン押下時のコマンド
        /// </summary>
        public ICommand HogeCommand => _hogeCommand ?? (_hogeCommand = new DelegateCommand<object>(_ =>
        {
            // hogeボタン押下後の処理
        }));
        private DelegateCommand<object> _hogeCommand;

        /// <summary>
        /// hugaボタン押下時のコマンド
        /// </summary>
        public ICommand HugaCommand => _hugaCommand ?? (_hugaCommand = new DelegateCommand<object>(_ =>
        {
            // hugaボタン押下後の処理
        }));
        private DelegateCommand<object> _hugaCommand;

        /// <summary>
        /// piyoボタン押下時のコマンド
        /// </summary>
        public ICommand PiyoCommand => _piyoCommand ?? (_piyoCommand = new DelegateCommand<object>(_ =>
        {
            // piyoボタン押下後の処理
        }));
        private DelegateCommand<object> _piyoCommand;

        /// <summary>
        /// 外部ファイルのデータを元にボタンガイド情報を設定する
        /// </summary>
        /// <param name="buttonGuides">ボタンガイド情報の表示内容が定義された外部のxmlファイルから読み込んだデータ</param>
        private void SetButtonGuide(ButtonGuideXmlData xmlData)
        {
            if (xmlData == null) return;

            // Hogeボタン         
            HogeButtonToolTip.Title = xmlData.Title;
            HogeButtonToolTip.Text = xmlData.Text;
            HogeButtonToolTip.Url = xmlData.LinUrl;
            HogeButtonToolTip.UrlText = xmlData.UrlText;

            // Hugaボタン         
            HugaButtonToolTip.Title = xmlData.Title;
            HugaButtonToolTip.Text = xmlData.Text;
            HugaButtonToolTip.Url = xmlData.LinUrl;
            HugaButtonToolTip.UrlText = xmlData.UrlText;

            // Piyoボタン         
            PiyoButtonToolTip.Title = xmlData.Title;
            PiyoButtonToolTip.Text = xmlData.Text;
            PiyoButtonToolTip.Url = xmlData.LinUrl;
            PiyoButtonToolTip.UrlText = xmlData.UrlText;
        }

ボタンガイド情報に関するデータクラス


    public class ButtonTooltip
    {
        /// <summary>
        /// ツールチップのタイトル
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// ツールチップの文章
        /// </summary>
        public string Text { get; set; }

        /// <summary>
        /// ツールチップのURLの文章
        /// </summary>
        public string UrlText { get; set; }

        /// <summary>
        /// ツールチップのURL
        /// </summary>
        public string Url { get; set; }
    }
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?