概要
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; }
}