TextBlock复杂的使用,请参考https://www.cnblogs.com/dalgleish/p/18995027
由于Avalonia没有内置Hyperlink,所以我们自己实现一个就行了。老规矩,留了一个坑,大家可以增加功能实现超链接。
Hyperlink类,移动端需要自己单独实现。(这个版本之后会被弃用)
public class Hyperlink : InlineUIContainer
{
private readonly TextBlock textBlock;
public event EventHandler<PointerPressedEventArgs>? Click;
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<Hyperlink, string>(
nameof(Text), defaultBindingMode: BindingMode.TwoWay);
public string Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public Hyperlink()
{
textBlock = new TextBlock
{
TextDecorations = new TextDecorationCollection
{
new TextDecoration { Location = TextDecorationLocation.Underline }
},
Foreground = Brushes.Blue,
Cursor = new Cursor(StandardCursorType.Hand)
};
// 绑定 Text 属性
this.GetObservable(TextProperty).Subscribe(t => textBlock.Text = t ?? string.Empty);
textBlock.PointerPressed += OnPointerPressed;
this.Child = textBlock;
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
Click?.Invoke(this, e);
}
private bool IsValidHttpUrl()
{
if (Uri.TryCreate(Text, UriKind.Absolute, out var uri))
{
return uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps;
}
return false;
}
public void Navigate()
{
if (!IsValidHttpUrl())
return;
try
{
// Windows / Linux / macOS 通用跳转方式
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = Text,
UseShellExecute = true
};
Process.Start(psi);
}
catch (Exception ex)
{
Console.WriteLine($"无法打开链接{Text}: {ex.Message}");
}
}
}
进阶版本(内嵌浏览器,浏览器WebBrowser类会在之后的章节里给出,项目也会改成用NavigationService进行导航,此时可以暂时不用学习这个进阶版本)
NavigationService类
using System; using System.Collections.Concurrent; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; namespace Shares.Avalonia; public static class NavigationService { private sealed class Relation { public object? Host; public object? Parent; } private static readonly ConcurrentDictionary<(Assembly assembly, string pageName), Type?> cache = new(); private static readonly ConditionalWeakTable<object, Relation> relationMap = new(); public static void Navigate(object current, string pageAxaml, bool fNewWindow = true, params object?[] ctorArgs) { try { if (current is null) return; var hostWindow = ResolveHostWindow(current); if (hostWindow is null) return; var ownerWindow = ResolveOwnerWindowForModal(current) ?? hostWindow; if (ownerWindow is null) return; var pageName = NormalizePageName(pageAxaml); if (string.IsNullOrWhiteSpace(pageName)) return; var targetType = ResolvePageType(hostWindow, pageName); if (targetType is null) return; var instance = CreateInstance(targetType, ctorArgs); if (instance is null) return; if (fNewWindow) { _ = ShowModalLikeMessageBoxAsync(ownerWindow, instance); return; } _ = Dispatcher.UIThread.InvokeAsync(() => ReplaceFromCurrent(current, instance, hostWindow)); } catch (Exception ex) { Console.WriteLine($"[NavigationService.Navigate] 导航主流程异常: {ex}"); } } private static object? CreateInstance(Type targetType, object?[] ctorArgs) { try { if (ctorArgs is { Length: > 0 }) return Activator.CreateInstance(targetType, ctorArgs); return Activator.CreateInstance(targetType); } catch (Exception ex) { Console.WriteLine( $"[NavigationService.CreateInstance] 创建实例失败: {targetType.FullName}, args={ctorArgs?.Length ?? 0}, ex={ex}"); return null; } } private static Window? ResolveHostWindow(object current) { try { if (current is Window w) return w; if (current is Control c) return TopLevel.GetTopLevel(c) as Window; if (TryGetHost(current, out var host) && host is Window hw) return hw; if (TryGetParent(current, out var parent)) return ResolveHostWindow(parent); } catch (Exception ex) { Console.WriteLine($"[ResolveHostWindow] 推导 HostWindow 失败: {ex}"); } return null; } private static Window? ResolveOwnerWindowForModal(object current) { try { var main = GetMainWindow(); if (current is Window w) { if (w.IsVisible) return w; if (main is not null && main.IsVisible) return main; return w; } if (current is Control c) { var top = TopLevel.GetTopLevel(c) as Window; if (top is not null && top.IsVisible) return top; if (main is not null && main.IsVisible) return main; return top ?? main; } if (TryGetHost(current, out var host) && host is Window hw) return hw.IsVisible ? hw : (main?.IsVisible == true ? main : hw); if (TryGetParent(current, out var parent)) return ResolveOwnerWindowForModal(parent); return main; } catch (Exception ex) { Console.WriteLine($"[ResolveOwnerWindowForModal] 推导 OwnerWindow 失败: {ex}"); return null; } } private static Window? GetMainWindow() { try { if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) return desktop.MainWindow; } catch (Exception ex) { Console.WriteLine($"[GetMainWindow] 获取 MainWindow 失败: {ex}"); } return null; } private static string NormalizePageName(string pageAxaml) { try { var name = pageAxaml?.Trim() ?? string.Empty; if (name.Length == 0) return string.Empty; name = Path.GetFileName(name); if (name.EndsWith(".axaml", true, CultureInfo.InvariantCulture)) name = Path.GetFileNameWithoutExtension(name); return name; } catch (Exception ex) { Console.WriteLine($"[NormalizePageName] 解析页面名失败: {pageAxaml}, ex={ex}"); return string.Empty; } } private static Type? ResolvePageType(Window hostWindow, string pageName) { try { var t = ResolveType(hostWindow.GetType().Assembly, pageName); if (t is not null) return t; t = ResolveType(Assembly.GetEntryAssembly(), pageName); if (t is not null) return t; t = ResolveType(typeof(NavigationService).Assembly, pageName); if (t is not null) return t; return ResolvePageTypeAllAssemblies(pageName); } catch (Exception ex) { Console.WriteLine($"[ResolvePageType] 查找页面类型失败: {pageName}, ex={ex}"); return null; } } private static Type? ResolvePageTypeAllAssemblies(string pageName) { try { foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) { // 跳过动态程序集/反射可能抛异常的情况由 ResolveType 内部处理 var t = ResolveType(asm, pageName); if (t is not null) return t; } } catch (Exception ex) { Console.WriteLine($"[ResolvePageTypeAllAssemblies] 扫描所有程序集失败: page={pageName}, ex={ex}"); } return null; } private static Type? ResolveType(Assembly? assembly, string pageName) { if (assembly is null) return null; return cache.GetOrAdd((assembly, pageName), key => { try { return key.assembly .GetTypes() .FirstOrDefault(t => string.Equals(t.Name, key.pageName, StringComparison.OrdinalIgnoreCase)); } catch (Exception ex) { Console.WriteLine( $"[ResolveType] 扫描程序集失败: {assembly.FullName}, page={pageName}, ex={ex}"); return null; } }); } private static async Task ShowModalLikeMessageBoxAsync(Window owner, object instance) { try { var realOwner = ResolveBestOwnerForModal(owner); if (realOwner is null) return; if (!realOwner.IsVisible) { var ok = await WaitForOwnerVisibleWithTimeout(realOwner, 2000); if (!ok || !realOwner.IsVisible) { var main = GetMainWindow(); if (main is not null && main.IsVisible) realOwner = main; else { await Dispatcher.UIThread.InvokeAsync( () => ShowNonModalFallback(instance)); return; } } } await Dispatcher.UIThread.InvokeAsync( () => ShowDialogCore(realOwner, instance)); } catch (Exception ex) { Console.WriteLine($"[ShowModalLikeMessageBoxAsync] 显示模态窗口失败: {ex}"); } } private static Window? ResolveBestOwnerForModal(Window owner) { try { if (owner.IsVisible) return owner; var main = GetMainWindow(); if (main is not null && main.IsVisible) return main; return owner; } catch (Exception ex) { Console.WriteLine($"[ResolveBestOwnerForModal] 选择 Owner 失败: {ex}"); return null; } } private static void ShowDialogCore(Window owner, object instance) { try { if (!owner.IsVisible) return; if (instance is Window window) { window.Icon = owner.Icon; window.WindowStartupLocation = WindowStartupLocation.CenterOwner; _ = window.ShowDialog(owner); return; } var content = EnsureControl(instance); if (content is null) return; var hostWindow = new Window { Icon = owner.Icon, WindowStartupLocation = WindowStartupLocation.CenterOwner, Content = content }; _ = hostWindow.ShowDialog(owner); } catch (Exception ex) { Console.WriteLine($"[ShowDialogCore] ShowDialog 执行失败: {ex}"); } } private static void ShowNonModalFallback(object instance) { try { if (instance is Window w) { w.Show(); return; } var content = EnsureControl(instance); if (content is null) return; new Window { Content = content }.Show(); } catch (Exception ex) { Console.WriteLine($"[ShowNonModalFallback] 非模态显示失败: {ex}"); } } private static async Task<bool> WaitForOwnerVisibleWithTimeout(Window owner, int timeoutMs) { try { if (owner.IsVisible) return true; var tcs = new TaskCompletionSource<bool>(); void OnOpened(object? s, EventArgs e) { owner.Opened -= OnOpened; tcs.TrySetResult(true); } owner.Opened += OnOpened; var delay = Task.Delay(timeoutMs); var done = await Task.WhenAny(tcs.Task, delay); return done == tcs.Task && owner.IsVisible; } catch (Exception ex) { Console.WriteLine($"[WaitForOwnerVisibleWithTimeout] 等待 Owner 可见失败: {ex}"); return false; } } private static Control? EnsureControl(object instance) { try { if (instance is Window w) return w.Content as Control; if (instance is Control c) return c; } catch (Exception ex) { Console.WriteLine($"[EnsureControl] 提取 Control 失败: {ex}"); } return null; } private static void ReplaceFromCurrent(object current, object instance, Window fallbackHostWindow) { try { var content = EnsureControl(instance); if (content is null) return; if (TryGetHost(current, out var cachedHost) && TryReplaceOnHost(cachedHost, content)) { CacheRelations(current, instance, content, cachedHost); return; } object? host = current switch { Window w => w, ContentControl cc => cc, Control c => FindNearestContentHost(c), _ => null }; if (host is null && TryGetParent(current, out var parent)) { if (TryGetHost(parent, out var parentHost)) host = parentHost; else if (parent is Control pc) host = FindNearestContentHost(pc); else if (parent is Window pw) host = pw; } host ??= fallbackHostWindow; if (TryReplaceOnHost(host, content)) CacheRelations(current, instance, content, host); } catch (Exception ex) { Console.WriteLine($"[ReplaceFromCurrent] 替换 Content 失败: {ex}"); } } private static void CacheRelations(object current, object instance, Control content, object host) { CacheHost(current, host); CacheHost(instance, host); CacheHost(content, host); CacheParent(instance, current); CacheParent(content, current); } private static bool TryReplaceOnHost(object host, Control content) { try { if (host is Window w) { w.Content = content; return true; } if (host is ContentControl cc) { cc.Content = content; return true; } } catch (Exception ex) { Console.WriteLine($"[TryReplaceOnHost] 写入 Content 失败: {ex}"); } return false; } private static object? FindNearestContentHost(Control start) { try { Control? cur = start; while (cur != null) { if (cur is ContentControl or Window) return cur; cur = cur.Parent as Control; } if (TryGetParent(start, out var parent)) { if (parent is Control pc) return FindNearestContentHost(pc); if (parent is Window pw) return pw; } return TopLevel.GetTopLevel(start) as Window; } catch (Exception ex) { Console.WriteLine($"[FindNearestContentHost] 查找 Host 失败: {ex}"); return null; } } private static Relation GetOrCreateRelation(object key) { return relationMap.GetValue(key, _ => new Relation()); } private static bool TryGetHost(object key, out object host) { if (relationMap.TryGetValue(key, out var relation) && relation.Host is not null) { host = relation.Host; return true; } host = null!; return false; } private static void CacheHost(object key, object host) { GetOrCreateRelation(key).Host = host; } private static void CacheParent(object child, object parent) { GetOrCreateRelation(child).Parent = parent; } private static bool TryGetParent(object child, out object parent) { if (relationMap.TryGetValue(child, out var relation) && relation.Parent is not null) { parent = relation.Parent; return true; } parent = null!; return false; } }
public class Hyperlink : InlineUIContainer { public sealed class WebBrowserPage : UserControl { public WebBrowserPage(string url) { Content = new CustomControls.WebBrowser { Address = url, HomeUrl = url }; } } private readonly TextBlock textBlock; private ContextMenu? contextMenu; public event EventHandler<PointerPressedEventArgs>? Click; public static readonly StyledProperty<string?> TextProperty = AvaloniaProperty.Register<Hyperlink, string?>(nameof(Text)); public string? Text { get => GetValue(TextProperty); set => SetValue(TextProperty, value); } public static readonly StyledProperty<string?> ContentTextProperty = AvaloniaProperty.Register<Hyperlink, string?>(nameof(ContentText)); [Content] public string? ContentText { get => GetValue(ContentTextProperty); set => SetValue(ContentTextProperty, value); } public static readonly StyledProperty<string?> NavigateUriProperty = AvaloniaProperty.Register<Hyperlink, string?>(nameof(NavigateUri)); public string? NavigateUri { get => GetValue(NavigateUriProperty); set => SetValue(NavigateUriProperty, value); } // 语义统一: // http(s):true=外部浏览器,false=内置 WebBrowser // 非 http(s):true/false 直接传给 NavigationService public static readonly StyledProperty<bool> OpenInNewWindowProperty = AvaloniaProperty.Register<Hyperlink, bool>(nameof(OpenInNewWindow), true); public bool OpenInNewWindow { get => GetValue(OpenInNewWindowProperty); set => SetValue(OpenInNewWindowProperty, value); } // 内置 WebBrowser 承载页名(Type.Name) public static readonly StyledProperty<string> WebBrowserPageNameProperty = AvaloniaProperty.Register<Hyperlink, string>( nameof(WebBrowserPageName), nameof(WebBrowserPage)); public string WebBrowserPageName { get => GetValue(WebBrowserPageNameProperty); set => SetValue(WebBrowserPageNameProperty, value); } public static readonly StyledProperty<double> OpacityProperty = AvaloniaProperty.Register<Hyperlink, double>(nameof(Opacity), 1.0); public double Opacity { get => GetValue(OpacityProperty); set => SetValue(OpacityProperty, value); } public Hyperlink() { textBlock = new TextBlock { TextDecorations = new TextDecorationCollection { new TextDecoration { Location = TextDecorationLocation.Underline } }, Foreground = Brushes.Blue, Cursor = new Cursor(StandardCursorType.Hand), Margin = new Thickness(0, 0, 0, -2), Transitions = new Transitions { new DoubleTransition { Property = OpacityProperty, Duration = TimeSpan.FromSeconds(0.5), Easing = new SineEaseInOut() } } }; this.GetObservable(TextProperty).Subscribe(_ => UpdateText()); this.GetObservable(ContentTextProperty).Subscribe(_ => UpdateText()); textBlock[!!TextBlock.OpacityProperty] = this[!!OpacityProperty]; textBlock.PointerPressed += OnPointerPressed; textBlock.PointerReleased += OnPointerReleased; Child = textBlock; UpdateText(); } private void UpdateText() { if (!string.IsNullOrWhiteSpace(Text)) { textBlock.Text = Text; return; } if (!string.IsNullOrWhiteSpace(ContentText)) { textBlock.Text = ContentText; return; } textBlock.Text = string.Empty; } private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { Click?.Invoke(this, e); if (!e.GetCurrentPoint(textBlock).Properties.IsRightButtonPressed) return; var target = NavigateUri; if (string.IsNullOrWhiteSpace(target)) return; if (!TryNormalizeHttpUrl(target, out _)) return; ShowContextMenu(); e.Handled = true; } private void OnPointerReleased(object? sender, PointerReleasedEventArgs e) { if (e.InitialPressMouseButton != MouseButton.Left) return; HandleNavigateFromClick(e.KeyModifiers); e.Handled = true; } private void HandleNavigateFromClick(KeyModifiers modifiers) { var target = NavigateUri; if (string.IsNullOrWhiteSpace(target)) return; var ctrl = (modifiers & KeyModifiers.Control) != 0; if (TryNormalizeHttpUrl(target, out var httpUrl)) { if (ctrl) { TryOpenExternalUrl(httpUrl); return; } if (OpenInNewWindow) { TryOpenExternalUrl(httpUrl); return; } NavigationService.Navigate( textBlock, WebBrowserPageName, fNewWindow: false, ctorArgs: new object?[] { httpUrl }); return; } var openNew = ctrl || OpenInNewWindow; NavigationService.Navigate(textBlock, target, openNew); } private void ShowContextMenu() { var target = NavigateUri; if (string.IsNullOrWhiteSpace(target)) return; if (!TryNormalizeHttpUrl(target, out _)) return; if (contextMenu == null) contextMenu = BuildContextMenu(); contextMenu.PlacementTarget = textBlock; contextMenu.Open(textBlock); } private ContextMenu BuildContextMenu() { var copyItem = new MenuItem { Header = MenuText("Copy link") }; copyItem.Click += async (_, __) => { var target = NavigateUri; if (string.IsNullOrWhiteSpace(target)) return; var top = TopLevel.GetTopLevel(textBlock); var cb = top?.Clipboard; if (cb == null) return; await cb.SetTextAsync(target); }; var openExternalItem = new MenuItem { Header = MenuText("Open in new window") }; openExternalItem.Click += (_, __) => { var target = NavigateUri; if (TryNormalizeHttpUrl(target ?? "", out var httpUrl)) TryOpenExternalUrl(httpUrl); }; var openEmbeddedItem = new MenuItem { Header = MenuText("Open embedded") }; openEmbeddedItem.Click += (_, __) => { var target = NavigateUri; if (TryNormalizeHttpUrl(target ?? "", out var httpUrl)) { NavigationService.Navigate( textBlock, WebBrowserPageName, fNewWindow: false, ctorArgs: new object?[] { httpUrl }); } }; return new ContextMenu { Items = { copyItem, new Separator(), openExternalItem, openEmbeddedItem } }; } private static TextBlock MenuText(string text) { return new TextBlock { Text = text, TextDecorations = null }; } private static bool TryNormalizeHttpUrl(string target, out string normalized) { normalized = ""; if (!Uri.TryCreate(target, UriKind.Absolute, out var uri)) return false; if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps) return false; normalized = uri.ToString(); return true; } private static void TryOpenExternalUrl(string target) { try { Process.Start(new ProcessStartInfo { FileName = target, UseShellExecute = true }); } catch { } } }
PopupTest.axaml代码
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="300" Width="300" x:Class="AvaloniaUI.PopupTest" Title="PopupTest"> <Grid Margin="10"> <TextBlock TextWrapping="Wrap"> You can use a Popup to provide a link for a specific <Hyperlink Text="term" Click="run_MouseClicked"/> of interest. </TextBlock> <Popup Name="popLink" IsOpen="False" Placement="Pointer" IsLightDismissEnabled="True"> <Border Background="White" CornerRadius="5" Padding="10"> <Border.Transitions> <Transitions> <DoubleTransition Property="Opacity" Duration="0:0:0.5" /> </Transitions> </Border.Transitions> <TextBlock TextWrapping="Wrap"> Welcome to C# Avalonia: <Hyperlink Text="https://www.cnblogs.com/dalgleish" Click="Hyperlink_Click"></Hyperlink> </TextBlock> </Border> </Popup> </Grid> </Window>
PopupTest.axaml.cs代码
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Shares.Avalonia;
namespace AvaloniaUI;
public partial class PopupTest : Window
{
public PopupTest()
{
InitializeComponent();
}
private void run_MouseClicked(object? sender, PointerPressedEventArgs e)
{
popLink.IsOpen = true;
}
private void Hyperlink_Click(object? sender, PointerPressedEventArgs e)
{
((Hyperlink?)sender)?.Navigate();
}
}
运行效果

浙公网安备 33010602011771号