16 Commits
Author SHA1 Message Date
Matěj Kubíček 924f90e86d Added export option for printers, but it needs some tuning. 2026-06-10 17:00:00 +02:00
Matěj Kubíček b4346a1461 Fixed the exporting so that the text is actually visible. 2026-06-10 15:55:12 +02:00
Matěj Kubíček b54df1fd4c Fix an issue with exporting images, so now the descriptions will also appear there 2026-04-23 19:25:19 +02:00
Matěj Kubíček 5efa49584d Got rid of the extra project that wasn't supposed to be there. 2026-04-22 22:11:21 +02:00
Matěj Kubíček c2e1649387 Sort of fixed. The diagrams now actually get placed where they're supposed to, but just to be safe, the camera no longer gets teleported to the diagrams coordinates. 2026-04-22 22:00:55 +02:00
Matěj Kubíček 70ca51d9ef i cannot for the life of me fix the dang camera shooting off to god knows where 2026-04-21 21:19:44 +02:00
Matěj Kubíček 8a57680b0e rewrite into avalonia for linux support 2026-04-21 18:15:07 +02:00
Matěj Kubíček fadca1bd6e Tried to fix up the wiring placement, it is a bit better but still not ideal. 2026-04-10 12:52:04 +02:00
Matěj Kubíček 9a97079945 Fixed up the text creation placement 2026-04-10 12:46:06 +02:00
Matěj Kubíček 2bd215c91a "Infinite" workspace for diagrams 2026-03-29 11:05:30 +02:00
Matěj Kubíček b77d2f12aa Fixed an issue where if you dragged a device to the screen, it used the last selected instead, 2026-03-28 21:25:27 +01:00
Matěj Kubíček e46f38d153 Inverting ifs, using switchcases etc. The things the IDE likes more. 2026-03-28 21:22:31 +01:00
Matěj Kubíček 9e88ce1cee Added an app icon, for now the router image. 2026-03-28 21:16:08 +01:00
Matěj Kubíček f409ac732d Did some design changes 2026-03-28 21:10:48 +01:00
Matěj Kubíček bcf2e89e6d Added the L3 switch to the json, fixed up the servers a bit in the json as well 2026-03-28 21:01:24 +01:00
Matěj Kubíček 038eb43f1d Who could've guessed that saving the device info in the ndjsons with absolute paths would be a bad idea 2026-03-27 19:08:35 +01:00
27 changed files with 1560 additions and 886 deletions
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AvaloniaProject">
<option name="projectPerEditor">
<map>
<entry key="NetworkDiagram/EditDeviceWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/ExportOptionsWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/Localization/Strings.cs.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/Localization/Strings.en.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/MainWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagramAvalonia/App.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
<entry key="NetworkDiagramAvalonia/Views/MainWindow.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
</map>
</option>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="LocationsWithSilentDeleteHolder">
<option name="locations">
<list>
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0-windows/win-x64/publish" />
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0/linux-x64/publish" />
<option value="$PROJECT_DIR$/NetworkDiagram/bin/Release/net10.0/linux-arm64/publish" />
</list>
</option>
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+88
View File
@@ -0,0 +1,88 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NetworkDiagram.App"
RequestedThemeVariant="Light">
<Application.Styles>
<FluentTheme />
<!-- Custom Styles -->
<Style Selector="Button.HeaderButtonStyle">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="border"
Background="{TemplateBinding Background}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{TemplateBinding Content}" Margin="8,4"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Button.HeaderButtonStyle:pointerover /template/ Border#border">
<Setter Property="Background" Value="#20FFFFFF"/>
</Style>
<Style Selector="Button.HeaderButtonStyle:pressed /template/ Border#border">
<Setter Property="Background" Value="#40FFFFFF"/>
</Style>
<Style Selector="Button.ModernButtonStyle">
<Setter Property="Background" Value="{DynamicResource AccentBlue}"/>
<Setter Property="Foreground" Value="{DynamicResource TextLight}"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.SecondaryButtonStyle">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource PrimaryBlue}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PrimaryBlue}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
<Style Selector="ListBox.SidebarListBoxStyle">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="ListBox.SidebarListBoxStyle ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="CornerRadius" Value="6"/>
</Style>
</Application.Styles>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="/Localization/Strings.en.axaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Color Palette -->
<Color x:Key="PrimaryBlueColor">#004578</Color>
<Color x:Key="AccentBlueColor">#0078D7</Color>
<Color x:Key="LightBlueColor">#E1EEFA</Color>
<Color x:Key="GridBlueColor">#D0E2F2</Color>
<Color x:Key="BgWhiteColor">#F5F9FF</Color>
<Color x:Key="TextDarkColor">#1B1B1B</Color>
<Color x:Key="TextLightColor">#FFFFFF</Color>
<Color x:Key="BorderGrayColor">#CCCCCC</Color>
<SolidColorBrush x:Key="PrimaryBlue" Color="{StaticResource PrimaryBlueColor}"/>
<SolidColorBrush x:Key="AccentBlue" Color="{StaticResource AccentBlueColor}"/>
<SolidColorBrush x:Key="LightBlue" Color="{StaticResource LightBlueColor}"/>
<SolidColorBrush x:Key="GridBlue" Color="{StaticResource GridBlueColor}"/>
<SolidColorBrush x:Key="BgWhite" Color="{StaticResource BgWhiteColor}"/>
<SolidColorBrush x:Key="TextDark" Color="{StaticResource TextDarkColor}"/>
<SolidColorBrush x:Key="TextLight" Color="{StaticResource TextLightColor}"/>
<SolidColorBrush x:Key="BorderGray" Color="{StaticResource BorderGrayColor}"/>
</ResourceDictionary>
</Application.Resources>
</Application>
+23
View File
@@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace NetworkDiagram;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}
-13
View File
@@ -1,13 +0,0 @@
<Application x:Class="NetworkDiagram.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NetworkDiagram"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Localization/Strings.en.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
-12
View File
@@ -1,12 +0,0 @@
using System.Configuration;
using System.Data;
using System.Windows;
namespace NetworkDiagram;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
-10
View File
@@ -1,10 +0,0 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+26
View File
@@ -0,0 +1,26 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NetworkDiagram.EditDeviceWindow"
Title="{DynamicResource EditTitle}" Height="380" Width="420"
WindowStartupLocation="CenterOwner" CanResize="False"
Background="{DynamicResource BgWhite}"
Icon="/Assets/router.jpg">
<Grid Margin="25" RowDefinitions="Auto,Auto,Auto,Auto,Auto,*,Auto">
<TextBlock Text="{DynamicResource DeviceNameLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,15"/>
<TextBlock Grid.Row="2" Text="{DynamicResource DescLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="DescBox" Grid.Row="3" Margin="0,0,0,15"
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="60"/>
<TextBlock Grid.Row="4" Text="{DynamicResource IpLabel}" Margin="0,0,0,8" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryBlue}"/>
<TextBox x:Name="IpBox" Grid.Row="5" Margin="0,0,0,25"
AcceptsReturn="True" VerticalContentAlignment="Top" MinHeight="60"/>
<StackPanel Grid.Row="6" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="{DynamicResource CancelBtn}" Classes="SecondaryButtonStyle" Click="Cancel_Click" Width="90" Margin="0,0,12,0"/>
<Button Content="{DynamicResource SaveDialogBtn}" Classes="ModernButtonStyle" Click="Save_Click" Width="90"/>
</StackPanel>
</Grid>
</Window>
+41
View File
@@ -0,0 +1,41 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using NetworkDiagram.Models;
namespace NetworkDiagram
{
public partial class EditDeviceWindow : Window
{
private readonly PlacedDevice _device;
public EditDeviceWindow()
{
InitializeComponent();
_device = new PlacedDevice(); // Should not be used but needed for designer
}
public EditDeviceWindow(PlacedDevice device)
{
InitializeComponent();
_device = device;
NameBox.Text = device.Name;
DescBox.Text = device.Description;
IpBox.Text = string.Join(Environment.NewLine, device.IpAddresses);
}
private void Save_Click(object? sender, RoutedEventArgs e)
{
_device.Name = NameBox.Text ?? string.Empty;
_device.Description = DescBox.Text ?? string.Empty;
_device.IpAddresses = (IpBox.Text ?? string.Empty)
.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
Close(true);
}
private void Cancel_Click(object? sender, RoutedEventArgs e)
{
Close(false);
}
}
}
-28
View File
@@ -1,28 +0,0 @@
<Window x:Class="NetworkDiagram.EditDeviceWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{DynamicResource EditTitle}" Height="350" Width="400"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#F5F9FF">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{DynamicResource DeviceNameLabel}" Margin="0,0,0,5"/>
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,15" Padding="5"/>
<TextBlock Grid.Row="2" Text="{DynamicResource IpLabel}" Margin="0,0,0,5"/>
<TextBox x:Name="IpBox" Grid.Row="3" Margin="0,0,0,15" Padding="5"
AcceptsReturn="True" VerticalScrollBarVisibility="Auto"/>
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="{DynamicResource CancelBtn}" Click="Cancel_Click" Width="80" Margin="0,0,10,0" Padding="5"/>
<Button Content="{DynamicResource SaveDialogBtn}" Click="Save_Click" Width="80" Padding="5" Background="#0078D7" Foreground="White"/>
</StackPanel>
</Grid>
</Window>
-34
View File
@@ -1,34 +0,0 @@
using System;
using System.Linq;
using System.Windows;
using NetworkDiagram.Models;
namespace NetworkDiagram
{
public partial class EditDeviceWindow : Window
{
private PlacedDevice _device;
public EditDeviceWindow(PlacedDevice device)
{
InitializeComponent();
_device = device;
NameBox.Text = device.Name;
IpBox.Text = string.Join(Environment.NewLine, device.IpAddresses);
}
private void Save_Click(object sender, RoutedEventArgs e)
{
_device.Name = NameBox.Text;
_device.IpAddresses = IpBox.Text.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).ToList();
DialogResult = true;
Close();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}
+13
View File
@@ -0,0 +1,13 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NetworkDiagram.ExportOptionsWindow"
Title="{DynamicResource ExportOptionsTitle}" Width="400" Height="180"
WindowStartupLocation="CenterOwner" CanResize="False"
Background="{DynamicResource BgWhite}">
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="{DynamicResource ExportWhiteBgMsg}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center" Margin="0,0,0,10"/>
<Button Content="{DynamicResource ExportWhiteBtn}" Click="White_Click" HorizontalAlignment="Stretch" Classes="ModernButtonStyle"/>
<Button Content="{DynamicResource ExportTransparentBtn}" Click="Transparent_Click" HorizontalAlignment="Stretch" Classes="SecondaryButtonStyle"/>
<Button Content="{DynamicResource ExportBWBtn}" Click="BlackAndWhite_Click" HorizontalAlignment="Stretch" Classes="SecondaryButtonStyle"/>
</StackPanel>
</Window>
@@ -0,0 +1,36 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace NetworkDiagram
{
public enum ExportOption
{
Cancel,
Transparent,
White,
BlackAndWhite
}
public partial class ExportOptionsWindow : Window
{
public ExportOptionsWindow()
{
InitializeComponent();
}
private void White_Click(object? sender, RoutedEventArgs e)
{
Close(ExportOption.White);
}
private void Transparent_Click(object? sender, RoutedEventArgs e)
{
Close(ExportOption.Transparent);
}
private void BlackAndWhite_Click(object? sender, RoutedEventArgs e)
{
Close(ExportOption.BlackAndWhite);
}
}
}
@@ -0,0 +1,29 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:String x:Key="AppTitle">Editor síťových diagramů</x:String>
<x:String x:Key="NewBtn">Nový</x:String>
<x:String x:Key="SaveBtn">Uložit</x:String>
<x:String x:Key="LoadBtn">Načíst</x:String>
<x:String x:Key="ExportBtn">Exportovat PNG</x:String>
<x:String x:Key="WireBtn">Kabelové připojení</x:String>
<x:String x:Key="WifiBtn">Bezdrátové (WiFi)</x:String>
<x:String x:Key="AddTextBtn">Přidat text</x:String>
<x:String x:Key="DevicesHeader">Zařízení</x:String>
<!-- Edit Dialog -->
<x:String x:Key="EditTitle">Upravit Zařízení</x:String>
<x:String x:Key="DeviceNameLabel">Název zařízení:</x:String>
<x:String x:Key="DescLabel">Popis:</x:String>
<x:String x:Key="IpLabel">IP Adresy (jedna na řádek):</x:String>
<x:String x:Key="CancelBtn">Zrušit</x:String>
<x:String x:Key="SaveDialogBtn">Uložit</x:String>
<!-- Messages -->
<x:String x:Key="ExportWhiteBgMsg">Vyberte styl pozadí a barev:</x:String>
<x:String x:Key="ExportWhiteBtn">Bílé pozadí</x:String>
<x:String x:Key="ExportTransparentBtn">Průhledné pozadí</x:String>
<x:String x:Key="ExportBWBtn">Pro tisk (šedotisk)</x:String>
<x:String x:Key="ExportOptionsTitle">Možnosti exportu</x:String>
<x:String x:Key="ExportSuccess">Export byl úspěšný!</x:String>
<x:String x:Key="EmptyDiagramMsg">Diagram je prázdný.</x:String>
</ResourceDictionary>
@@ -1,26 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="AppTitle">Editor síťových diagramů</system:String>
<system:String x:Key="NewBtn">Nový</system:String>
<system:String x:Key="SaveBtn">Uložit</system:String>
<system:String x:Key="LoadBtn">Načíst</system:String>
<system:String x:Key="ExportBtn">Exportovat PNG</system:String>
<system:String x:Key="WireBtn">Kabelové připojení</system:String>
<system:String x:Key="WifiBtn">Bezdrátové (WiFi)</system:String>
<system:String x:Key="AddTextBtn">Přidat text</system:String>
<system:String x:Key="DevicesHeader">Zařízení</system:String>
<!-- Edit Dialog -->
<system:String x:Key="EditTitle">Upravit zařízení</system:String>
<system:String x:Key="DeviceNameLabel">Název zařízení:</system:String>
<system:String x:Key="IpLabel">IP adresy (jedna na řádek):</system:String>
<system:String x:Key="CancelBtn">Zrušit</system:String>
<system:String x:Key="SaveDialogBtn">Uložit</system:String>
<!-- Messages -->
<system:String x:Key="ExportWhiteBgMsg">Chcete bílé pozadí? (Ne = průhlednost)</system:String>
<system:String x:Key="ExportOptionsTitle">Možnosti exportu</system:String>
<system:String x:Key="ExportSuccess">Export byl úspěšný!</system:String>
<system:String x:Key="EmptyDiagramMsg">Diagram je prázdný.</system:String>
</ResourceDictionary>
@@ -0,0 +1,29 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<x:String x:Key="AppTitle">Network Diagram Studio</x:String>
<x:String x:Key="NewBtn">New</x:String>
<x:String x:Key="SaveBtn">Save</x:String>
<x:String x:Key="LoadBtn">Load</x:String>
<x:String x:Key="ExportBtn">Export PNG</x:String>
<x:String x:Key="WireBtn">Wire Connection</x:String>
<x:String x:Key="WifiBtn">Wifi Connection</x:String>
<x:String x:Key="AddTextBtn">Add Text</x:String>
<x:String x:Key="DevicesHeader">Devices</x:String>
<!-- Edit Dialog -->
<x:String x:Key="EditTitle">Edit Device</x:String>
<x:String x:Key="DeviceNameLabel">Device Name:</x:String>
<x:String x:Key="DescLabel">Description:</x:String>
<x:String x:Key="IpLabel">IP Addresses (one per line):</x:String>
<x:String x:Key="CancelBtn">Cancel</x:String>
<x:String x:Key="SaveDialogBtn">Save</x:String>
<!-- Messages -->
<x:String x:Key="ExportWhiteBgMsg">Select export background and color style:</x:String>
<x:String x:Key="ExportWhiteBtn">White Background</x:String>
<x:String x:Key="ExportTransparentBtn">Transparent Background</x:String>
<x:String x:Key="ExportBWBtn">Printer Friendly (Grayscale)</x:String>
<x:String x:Key="ExportOptionsTitle">Export Options</x:String>
<x:String x:Key="ExportSuccess">Export successful!</x:String>
<x:String x:Key="EmptyDiagramMsg">Diagram is empty.</x:String>
</ResourceDictionary>
@@ -1,26 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="AppTitle">Network Diagram Studio</system:String>
<system:String x:Key="NewBtn">New</system:String>
<system:String x:Key="SaveBtn">Save</system:String>
<system:String x:Key="LoadBtn">Load</system:String>
<system:String x:Key="ExportBtn">Export PNG</system:String>
<system:String x:Key="WireBtn">Wire Connection</system:String>
<system:String x:Key="WifiBtn">Wifi Connection</system:String>
<system:String x:Key="AddTextBtn">Add Text</system:String>
<system:String x:Key="DevicesHeader">Devices</system:String>
<!-- Edit Dialog -->
<system:String x:Key="EditTitle">Edit Device</system:String>
<system:String x:Key="DeviceNameLabel">Device Name:</system:String>
<system:String x:Key="IpLabel">IP Addresses (one per line):</system:String>
<system:String x:Key="CancelBtn">Cancel</system:String>
<system:String x:Key="SaveDialogBtn">Save</system:String>
<!-- Messages -->
<system:String x:Key="ExportWhiteBgMsg">Do you want a white background? (No will result in transparency)</system:String>
<system:String x:Key="ExportOptionsTitle">Export Options</system:String>
<system:String x:Key="ExportSuccess">Export successful!</system:String>
<system:String x:Key="EmptyDiagramMsg">Diagram is empty.</system:String>
</ResourceDictionary>
+112
View File
@@ -0,0 +1,112 @@
<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"
xmlns:models="clr-namespace:NetworkDiagram.Models"
mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="750"
x:Class="NetworkDiagram.MainWindow"
Title="{DynamicResource AppTitle}"
Background="{DynamicResource BgWhite}"
Icon="/Assets/router.jpg"
KeyDown="Window_KeyDown">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="60"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Header -->
<Border Grid.Row="0" Grid.ColumnSpan="2" Background="{DynamicResource PrimaryBlue}" ZIndex="10">
<Border.BoxShadow>
<BoxShadows>0 2 10 0 #4D000000</BoxShadows>
</Border.BoxShadow>
<DockPanel Margin="15,0">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource AppTitle}" Foreground="White" FontSize="20" FontWeight="Bold" Margin="0,0,30,0" VerticalAlignment="Center"/>
<Button Content="{DynamicResource NewBtn}" Classes="HeaderButtonStyle" Click="NewDiagram_Click"/>
<Button Content="{DynamicResource SaveBtn}" Classes="HeaderButtonStyle" Click="SaveDiagram_Click"/>
<Button Content="{DynamicResource LoadBtn}" Classes="HeaderButtonStyle" Click="LoadDiagram_Click"/>
<Button Content="{DynamicResource ExportBtn}" Classes="HeaderButtonStyle" Click="ExportPng_Click"/>
<Rectangle Width="1" Height="24" Fill="White" Opacity="0.3" Margin="15,0"/>
<Button Content="{DynamicResource WireBtn}" Classes="HeaderButtonStyle" Click="WireTool_Click"/>
<Button Content="{DynamicResource WifiBtn}" Classes="HeaderButtonStyle" Click="WifiTool_Click"/>
<Button Content="{DynamicResource AddTextBtn}" Classes="HeaderButtonStyle" Click="AddText_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<ComboBox x:Name="LangCombo" Width="80" Height="34" SelectionChanged="LangCombo_SelectionChanged" VerticalContentAlignment="Center">
<ComboBoxItem Tag="en">EN</ComboBoxItem>
<ComboBoxItem Tag="cs">CZ</ComboBoxItem>
</ComboBox>
</StackPanel>
</DockPanel>
</Border>
<!-- Sidebar -->
<Border Grid.Row="1" Grid.Column="0" Background="{DynamicResource LightBlue}" BorderBrush="{DynamicResource GridBlue}" BorderThickness="0,0,1,0">
<DockPanel Margin="10,15">
<TextBlock Text="{DynamicResource DevicesHeader}" DockPanel.Dock="Top" FontSize="18" FontWeight="Bold" Margin="10,0,10,15" Foreground="{DynamicResource PrimaryBlue}"/>
<ListBox x:Name="ToolboxList" Classes="SidebarListBoxStyle"
PointerMoved="Toolbox_PointerMoved">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:DeviceTemplate">
<Border Background="White" CornerRadius="8" Padding="12" BorderBrush="{DynamicResource GridBlue}" BorderThickness="1">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding IconPath, Converter={x:Static models:ImageConverter.Instance}}" Width="36" Height="36" Margin="0,0,12,0"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource TextDark}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>
<!-- Canvas Area -->
<Panel Grid.Row="1" Grid.Column="1" x:Name="CanvasViewport" ClipToBounds="True" Background="Transparent"
DragDrop.AllowDrop="True"
PointerPressed="Viewport_PointerPressed"
PointerMoved="Viewport_PointerMoved"
PointerReleased="Viewport_PointerReleased"
PointerWheelChanged="Viewport_PointerWheelChanged">
<Canvas x:Name="DiagramCanvas" Width="40000" Height="40000"
PointerPressed="DiagramCanvas_PointerPressed"
PointerMoved="DiagramCanvas_PointerMoved"
PointerReleased="DiagramCanvas_PointerReleased">
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Canvas.RenderTransform>
<Canvas.Background>
<DrawingBrush TileMode="Tile" SourceRect="0,0,40,40" DestinationRect="0,0,40,40">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Geometry>
<PathGeometry Figures="M 0 40 L 0 0 L 40 0" />
</GeometryDrawing.Geometry>
<GeometryDrawing.Pen>
<Pen Brush="{DynamicResource GridBlue}" Thickness="0.8"/>
</GeometryDrawing.Pen>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Canvas.Background>
<!-- Selection rectangle visual -->
<Rectangle x:Name="SelectionRect" Stroke="{DynamicResource AccentBlue}" StrokeThickness="1.5" StrokeDashArray="3,3"
Fill="#220078D7" IsVisible="False" ZIndex="9999" IsHitTestVisible="False"/>
</Canvas>
</Panel>
</Grid>
</Window>
+954
View File
@@ -0,0 +1,954 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System.Runtime.InteropServices;
using NetworkDiagram.Models;
using Avalonia.Controls.Templates;
namespace NetworkDiagram
{
public partial class MainWindow : Window
{
private List<DeviceTemplate> _deviceTemplates = [];
private Diagram _currentDiagram = new();
private bool _langComboReady;
// Selection & Dragging
private readonly HashSet<Control> _selectedElements = [];
private Point _dragStartPoint;
private bool _isDraggingDevices;
private bool _isSelectingArea;
private Point _selectionStartPoint;
private ConnectionType? _activeTool;
private PlacedDevice? _firstDeviceForConnection;
private readonly Dictionary<Connection, Line> _connectionLines = new();
private readonly Dictionary<PlacedDevice, Control> _deviceElements = new();
// Panning and Zooming
private Point _lastPanPoint;
private bool _isPanning;
private TranslateTransform CanvasTranslate = null!;
private ScaleTransform CanvasScale = null!;
public MainWindow()
{
InitializeComponent();
LangCombo.SelectedIndex = 0;
_langComboReady = true;
LoadTemplates();
this.Opened += (s, e) => {
if (DiagramCanvas.RenderTransform is TransformGroup group)
{
CanvasScale = group.Children.OfType<ScaleTransform>().First();
CanvasTranslate = group.Children.OfType<TranslateTransform>().First();
}
CenterView();
};
AddHandler(DragDrop.DragOverEvent, Viewport_DragOver);
AddHandler(DragDrop.DropEvent, Viewport_Drop);
// Use Tunneling for drag start to avoid ListBoxItem selection interference
ToolboxList.AddHandler(PointerPressedEvent, Toolbox_PointerPressed, RoutingStrategies.Tunnel);
}
// The logical center of the 40000x40000 canvas
private const double CanvasCenterX = 20000;
private const double CanvasCenterY = 20000;
private void CenterView()
{
if (CanvasTranslate == null || CanvasScale == null) return;
CanvasScale.ScaleX = 1.0;
CanvasScale.ScaleY = 1.0;
// Změna na 0,0 aby levý horní roh plátna odpovídal levému hornímu rohu viewportu
CanvasTranslate.X = 0;
CanvasTranslate.Y = 0;
}
#region Panning and Zooming
private void Viewport_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var properties = e.GetCurrentPoint(CanvasViewport).Properties;
if (properties.IsMiddleButtonPressed || properties.IsRightButtonPressed)
{
_isPanning = true;
_lastPanPoint = e.GetPosition(CanvasViewport);
e.Pointer.Capture(CanvasViewport);
e.Handled = true;
}
}
private void Viewport_PointerMoved(object? sender, PointerEventArgs e)
{
if (_isPanning)
{
Point currentPoint = e.GetPosition(CanvasViewport);
double deltaX = currentPoint.X - _lastPanPoint.X;
double deltaY = currentPoint.Y - _lastPanPoint.Y;
CanvasTranslate.X += deltaX;
CanvasTranslate.Y += deltaY;
_lastPanPoint = currentPoint;
}
}
private void Viewport_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
var props = e.GetCurrentPoint(CanvasViewport).Properties;
if (!props.IsMiddleButtonPressed && !props.IsRightButtonPressed)
{
_isPanning = false;
e.Pointer.Capture(null);
}
}
private void Viewport_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (CanvasScale == null || CanvasTranslate == null) return;
double zoom = e.Delta.Y > 0 ? 1.1 : 0.9;
double newScale = CanvasScale.ScaleX * zoom;
if (newScale < 0.05 || newScale > 10) return;
Point mousePos = e.GetPosition(DiagramCanvas);
CanvasScale.ScaleX = newScale;
CanvasScale.ScaleY = newScale;
Point newMousePos = e.GetPosition(DiagramCanvas);
CanvasTranslate.X += (newMousePos.X - mousePos.X) * CanvasScale.ScaleX;
CanvasTranslate.Y += (newMousePos.Y - mousePos.Y) * CanvasScale.ScaleY;
}
#endregion
private void LoadTemplates()
{
try
{
if (!File.Exists("devices.json")) return;
var json = File.ReadAllText("devices.json");
_deviceTemplates = JsonSerializer.Deserialize<List<DeviceTemplate>>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? [];
ToolboxList.ItemsSource = _deviceTemplates;
}
catch (Exception ex) { Console.WriteLine($"Error loading templates: {ex.Message}"); }
}
private void LangCombo_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (!_langComboReady) return;
if (LangCombo.SelectedItem is ComboBoxItem { Tag: string lang })
{
var app = Application.Current;
if (app == null) return;
var mergedDicts = app.Resources.MergedDictionaries;
var oldDict = mergedDicts.OfType<ResourceInclude>().FirstOrDefault(ri => ri.Source?.OriginalString.Contains("Localization/Strings.") == true);
if (oldDict != null) mergedDicts.Remove(oldDict);
mergedDicts.Add(new ResourceInclude(new Uri("avares://NetworkDiagram/App.axaml"))
{
Source = new Uri($"avares://NetworkDiagram/Localization/Strings.{lang}.axaml")
});
}
}
#region Drag and Drop (Toolbox to Canvas)
private Point _toolboxDragStart;
private DeviceTemplate? _toolboxPendingTemplate;
private void Toolbox_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var item = (e.Source as Control)?.FindAncestorOfType<ListBoxItem>();
if (item?.Content is DeviceTemplate template)
{
_toolboxDragStart = e.GetPosition(null);
_toolboxPendingTemplate = template;
}
}
private async void Toolbox_PointerMoved(object? sender, PointerEventArgs e)
{
if (_toolboxPendingTemplate != null && e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
{
var pos = e.GetPosition(null);
var diff = _toolboxDragStart - pos;
if (Math.Abs(diff.X) > 5 || Math.Abs(diff.Y) > 5)
{
var template = _toolboxPendingTemplate;
_toolboxPendingTemplate = null;
var data = new DataObject();
data.Set("DeviceTemplate", template);
await DragDrop.DoDragDrop(e, data, DragDropEffects.Copy);
}
}
}
private void Viewport_DragOver(object? sender, DragEventArgs e)
{
if (e.Data.Contains("DeviceTemplate"))
e.DragEffects = DragDropEffects.Copy;
else
e.DragEffects = DragDropEffects.None;
}
private void Viewport_Drop(object? sender, DragEventArgs e)
{
if (e.Data.Get("DeviceTemplate") is DeviceTemplate template)
{
var dropPoint = e.GetPosition(DiagramCanvas);
AddDeviceToCanvas(template, dropPoint.X, dropPoint.Y);
}
}
private void DiagramCanvas_PointerPressed(object? sender, PointerPressedEventArgs e)
{
var props = e.GetCurrentPoint(DiagramCanvas).Properties;
if (e.Source == DiagramCanvas && props.IsLeftButtonPressed)
{
ClearSelection();
_isSelectingArea = true;
_selectionStartPoint = e.GetPosition(DiagramCanvas);
Canvas.SetLeft(SelectionRect, _selectionStartPoint.X);
Canvas.SetTop(SelectionRect, _selectionStartPoint.Y);
SelectionRect.Width = 0;
SelectionRect.Height = 0;
SelectionRect.IsVisible = true;
e.Pointer.Capture(DiagramCanvas);
}
}
private void DiagramCanvas_PointerMoved(object? sender, PointerEventArgs e)
{
var currentPoint = e.GetPosition(DiagramCanvas);
if (_isSelectingArea)
{
var x = Math.Min(_selectionStartPoint.X, currentPoint.X);
var y = Math.Min(_selectionStartPoint.Y, currentPoint.Y);
var w = Math.Abs(_selectionStartPoint.X - currentPoint.X);
var h = Math.Abs(_selectionStartPoint.Y - currentPoint.Y);
Canvas.SetLeft(SelectionRect, x);
Canvas.SetTop(SelectionRect, y);
SelectionRect.Width = w;
SelectionRect.Height = h;
var selectionBounds = new Rect(x, y, w, h);
foreach (var entry in _deviceElements)
{
var border = entry.Value;
var elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.Bounds.Width, border.Bounds.Height);
if (selectionBounds.Intersects(elementBounds))
{
if (!_selectedElements.Contains(border)) SelectElement(border);
}
else
{
if (_selectedElements.Contains(border)) DeselectElement(border);
}
}
}
else if (_isDraggingDevices)
{
var deltaX = currentPoint.X - _dragStartPoint.X;
var deltaY = currentPoint.Y - _dragStartPoint.Y;
foreach (var element in _selectedElements)
{
if (element.Tag is PlacedDevice device)
{
device.X += deltaX;
device.Y += deltaY;
}
}
_dragStartPoint = currentPoint;
}
}
private void DiagramCanvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
StopDragging();
e.Pointer.Capture(null);
}
#endregion
private Point GetVisibleCanvasCenter()
{
if (CanvasTranslate == null || CanvasScale == null) return new Point(CanvasCenterX, CanvasCenterY);
double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : 1000;
double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : 700;
return new Point(
(vw / 2 - CanvasTranslate.X) / CanvasScale.ScaleX,
(vh / 2 - CanvasTranslate.Y) / CanvasScale.ScaleY
);
}
private void AddDeviceToCanvas(DeviceTemplate template, double x, double y)
{
var placed = new PlacedDevice { Name = template.Name, TemplateName = template.Name, IconPath = template.IconPath, X = x, Y = y };
_currentDiagram.Devices.Add(placed);
RenderDevice(placed);
}
private void RenderDevice(PlacedDevice device)
{
var container = new Border
{
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12),
Background = Brushes.White,
BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!,
BorderThickness = new Thickness(1),
MinWidth = 100,
MinHeight = 80,
MaxWidth = 180,
Tag = device,
ZIndex = 10,
DataContext = device
};
var stack = new StackPanel { Spacing = 4 };
var image = new Image { Width = 64, Height = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
image.Bind(Image.SourceProperty, new Avalonia.Data.Binding("IconPath") { Converter = ImageConverter.Instance });
stack.Children.Add(image);
var primaryBlue = Application.Current!.FindResource("PrimaryBlue");
var nameText = new TextBlock { FontWeight = FontWeight.Bold, FontSize = 12, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center };
if (primaryBlue is IBrush brush) nameText.Foreground = brush;
nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Name"));
stack.Children.Add(nameText);
var textDark = Application.Current!.FindResource("TextDark");
var descText = new TextBlock { FontSize = 11, FontStyle = FontStyle.Italic, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center, Opacity = 0.8 };
if (textDark is IBrush td) descText.Foreground = td;
descText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Description"));
descText.Bind(TextBlock.IsVisibleProperty, new Avalonia.Data.Binding("Description") { Converter = Avalonia.Data.Converters.StringConverters.IsNotNullOrEmpty });
stack.Children.Add(descText);
var ips = new ItemsControl();
ips.Bind(ItemsControl.ItemsSourceProperty, new Avalonia.Data.Binding("IpAddresses"));
var accentBlue = Application.Current!.FindResource("AccentBlue");
ips.ItemTemplate = new FuncDataTemplate<string>((val, _) => {
var tb = new TextBlock { Text = val, FontSize = 10, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Opacity = 0.8 };
if (accentBlue is IBrush b) tb.Foreground = b;
return tb;
});
stack.Children.Add(ips);
container.Child = stack;
Canvas.SetLeft(container, device.X);
Canvas.SetTop(container, device.Y);
device.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(PlacedDevice.X)) Canvas.SetLeft(container, device.X);
if (e.PropertyName == nameof(PlacedDevice.Y)) Canvas.SetTop(container, device.Y);
};
container.PointerPressed += (s, e) =>
{
if (_activeTool != null)
{
HandleConnectionTool(device, container);
e.Handled = true;
return;
}
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control))
{
if (!_selectedElements.Contains(container))
{
ClearSelection();
SelectElement(container);
}
}
else
{
if (_selectedElements.Contains(container)) DeselectElement(container);
else SelectElement(container);
}
_isDraggingDevices = true;
_dragStartPoint = e.GetPosition(DiagramCanvas);
e.Pointer.Capture(DiagramCanvas);
e.Handled = true;
if (e.ClickCount == 2)
{
StopDragging();
EditDevice(device);
}
};
_deviceElements[device] = container;
DiagramCanvas.Children.Add(container);
}
private void StopDragging()
{
_isDraggingDevices = false;
_isSelectingArea = false;
SelectionRect.IsVisible = false;
}
private void SelectElement(Control element)
{
_selectedElements.Add(element);
if (element is Border b)
{
var accentBlue = Application.Current!.FindResource("AccentBlue");
if (accentBlue is IBrush brush) b.BorderBrush = brush;
b.Background = new SolidColorBrush(Color.FromArgb(50, 0, 120, 215));
b.BorderThickness = new Thickness(2);
}
else if (element is Line l)
{
l.StrokeThickness = 5;
l.Opacity = 0.8;
}
}
private void DeselectElement(Control element)
{
_selectedElements.Remove(element);
if (element is Border b)
{
b.BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!;
b.Background = Brushes.White;
b.BorderThickness = new Thickness(1);
}
else if (element is Line l)
{
l.StrokeThickness = 3;
l.Opacity = 1.0;
}
}
private void ClearSelection()
{
foreach (var el in _selectedElements.ToList()) DeselectElement(el);
}
private void HandleConnectionTool(PlacedDevice device, Control element)
{
if (_firstDeviceForConnection == null)
{
_firstDeviceForConnection = device;
element.Opacity = 0.5;
}
else
{
if (_firstDeviceForConnection != device)
{
var conn = new Connection { StartDevice = _firstDeviceForConnection, EndDevice = device, Type = _activeTool!.Value };
_currentDiagram.Connections.Add(conn);
RenderConnection(conn);
}
ResetConnectionTool();
}
}
private void ResetConnectionTool()
{
_firstDeviceForConnection = null;
_activeTool = null;
foreach (var child in _deviceElements.Values) child.Opacity = 1.0;
}
private void RenderConnection(Connection conn)
{
var line = new Line
{
Stroke = conn.Type == ConnectionType.Wifi ? Brushes.DeepSkyBlue : Brushes.SlateGray,
StrokeThickness = 3,
Tag = conn,
ZIndex = 0
};
if (conn.Type == ConnectionType.Wifi) line.StrokeDashArray = new AvaloniaList<double>(new[] { 2.0, 2.0 });
void UpdatePos()
{
if (!_deviceElements.TryGetValue(conn.StartDevice, out var startEl) || !_deviceElements.TryGetValue(conn.EndDevice, out var endEl)) return;
double sw = startEl.Bounds.Width > 0 ? startEl.Bounds.Width : 120;
double sh = startEl.Bounds.Height > 0 ? startEl.Bounds.Height : 100;
double ew = endEl.Bounds.Width > 0 ? endEl.Bounds.Width : 120;
double eh = endEl.Bounds.Height > 0 ? endEl.Bounds.Height : 100;
line.StartPoint = new Point(conn.StartDevice.X + sw / 2, conn.StartDevice.Y + sh / 2);
line.EndPoint = new Point(conn.EndDevice.X + ew / 2, conn.EndDevice.Y + eh / 2);
}
UpdatePos();
conn.StartDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); };
conn.EndDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); };
// Re-calculate line endpoints once device bounds are known after first layout pass
// Unsubscribe immediately after first call to avoid repeated triggers
if (_deviceElements.TryGetValue(conn.StartDevice, out var startElem))
{
EventHandler? handler = null;
handler = (s, e) => { startElem.LayoutUpdated -= handler; UpdatePos(); };
startElem.LayoutUpdated += handler;
}
if (_deviceElements.TryGetValue(conn.EndDevice, out var endElem) && endElem != startElem)
{
EventHandler? handler = null;
handler = (s, e) => { endElem.LayoutUpdated -= handler; UpdatePos(); };
endElem.LayoutUpdated += handler;
}
line.PointerPressed += (s, e) => {
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) ClearSelection();
SelectElement(line);
e.Handled = true;
};
_connectionLines[conn] = line;
DiagramCanvas.Children.Insert(0, line);
}
private async void EditDevice(PlacedDevice device)
{
var dialog = new EditDeviceWindow(device);
await dialog.ShowDialog(this);
if (_deviceElements.TryGetValue(device, out var elem))
{
var attached = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
foreach(var c in attached) if (_connectionLines.TryGetValue(c, out var l))
{
double sw = _deviceElements[c.StartDevice].Bounds.Width > 0 ? _deviceElements[c.StartDevice].Bounds.Width : 120;
double sh = _deviceElements[c.StartDevice].Bounds.Height > 0 ? _deviceElements[c.StartDevice].Bounds.Height : 100;
double ew = _deviceElements[c.EndDevice].Bounds.Width > 0 ? _deviceElements[c.EndDevice].Bounds.Width : 120;
double eh = _deviceElements[c.EndDevice].Bounds.Height > 0 ? _deviceElements[c.EndDevice].Bounds.Height : 100;
l.StartPoint = new Point(c.StartDevice.X + sw / 2, c.StartDevice.Y + sh / 2);
l.EndPoint = new Point(c.EndDevice.X + ew / 2, c.EndDevice.Y + eh / 2);
}
}
}
private void Window_KeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Delete && _selectedElements.Count > 0)
{
foreach (var el in _selectedElements.ToList())
{
if (el.Tag is Connection conn && el is Line line)
{
_currentDiagram.Connections.Remove(conn);
_connectionLines.Remove(conn);
DiagramCanvas.Children.Remove(line);
}
else if (el.Tag is PlacedDevice device)
{
_currentDiagram.Devices.Remove(device);
_deviceElements.Remove(device);
var toRemove = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
foreach (var c in toRemove)
{
if (_connectionLines.TryGetValue(c, out var l)) DiagramCanvas.Children.Remove(l);
_currentDiagram.Connections.Remove(c);
_connectionLines.Remove(c);
}
DiagramCanvas.Children.Remove(el);
}
}
_selectedElements.Clear();
}
else if (e.Key == Key.Escape)
{
ResetConnectionTool(); StopDragging(); ClearSelection();
}
else if (e.Key == Key.Home)
{
if (_currentDiagram.Devices.Count > 0) CenterOnContent();
else CenterView();
}
}
#region Toolbar Events
private void NewDiagram_Click(object? sender, RoutedEventArgs e)
{
_currentDiagram = new Diagram();
DiagramCanvas.Children.Clear();
DiagramCanvas.Children.Add(SelectionRect);
_connectionLines.Clear();
_deviceElements.Clear();
ClearSelection();
CenterView();
}
private async void SaveDiagram_Click(object? sender, RoutedEventArgs e)
{
var topLevel = GetTopLevel(this);
if (topLevel == null) return;
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Save Network Diagram",
FileTypeChoices = new[] { new FilePickerFileType("Network Diagram") { Patterns = new[] { "*.ndjson" } } }
});
if (file != null)
{
var model = new DiagramSaveModel {
Devices = _currentDiagram.Devices,
Connections = _currentDiagram.Connections.Select(c => new ConnectionSaveModel {
StartIndex = _currentDiagram.Devices.IndexOf(c.StartDevice),
EndIndex = _currentDiagram.Devices.IndexOf(c.EndDevice),
Type = c.Type
}).ToList()
};
using var stream = await file.OpenWriteAsync();
await JsonSerializer.SerializeAsync(stream, model, new JsonSerializerOptions { WriteIndented = true });
}
}
private void CenterOnContent()
{
if (CanvasTranslate == null || CanvasScale == null || _currentDiagram.Devices.Count == 0) return;
var devices = _currentDiagram.Devices;
double minX = devices.Min(d => d.X);
double minY = devices.Min(d => d.Y);
double maxX = devices.Max(d => d.X) + 160;
double maxY = devices.Max(d => d.Y) + 120;
double contentWidth = maxX - minX;
double contentHeight = maxY - minY;
double contentCenterX = (minX + maxX) / 2.0;
double contentCenterY = (minY + maxY) / 2.0;
double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : (this.Bounds.Width > 240 ? this.Bounds.Width - 240 : 860);
double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : (this.Bounds.Height > 60 ? this.Bounds.Height - 60 : 600);
// Zoom-to-fit: scale content to fill 85% of viewport, capped between 0.05 and 2.0
double scaleToFit = Math.Min((vw * 0.85) / contentWidth, (vh * 0.85) / contentHeight);
double newScale = Math.Clamp(scaleToFit, 0.05, 2.0);
CanvasScale.ScaleX = newScale;
CanvasScale.ScaleY = newScale;
CanvasTranslate.X = vw / 2.0 - contentCenterX * newScale;
CanvasTranslate.Y = vh / 2.0 - contentCenterY * newScale;
}
private async void LoadDiagram_Click(object? sender, RoutedEventArgs e)
{
var topLevel = GetTopLevel(this);
if (topLevel == null) return;
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open Network Diagram",
FileTypeFilter = new[] { new FilePickerFileType("Network Diagram") { Patterns = new[] { "*.ndjson" } } }
});
if (files.Count > 0)
{
try {
using var stream = await files[0].OpenReadAsync();
var model = await JsonSerializer.DeserializeAsync<DiagramSaveModel>(stream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (model == null || model.Devices == null) return;
// Clear canvas without calling CenterView (we'll center on content after load)
_currentDiagram = new Diagram();
DiagramCanvas.Children.Clear();
DiagramCanvas.Children.Add(SelectionRect);
_connectionLines.Clear();
_deviceElements.Clear();
ClearSelection();
// Load devices exactly as saved — no normalization
_currentDiagram = new Diagram { Devices = model.Devices };
foreach (var device in _currentDiagram.Devices)
{
var template = _deviceTemplates.FirstOrDefault(t => t.Name == device.TemplateName);
if (template != null) device.IconPath = template.IconPath;
else if (device.TemplateName == "Note") device.IconPath = "";
RenderDevice(device);
}
if (model.Connections != null)
{
foreach (var cModel in model.Connections)
{
if (cModel.StartIndex >= 0 && cModel.StartIndex < _currentDiagram.Devices.Count &&
cModel.EndIndex >= 0 && cModel.EndIndex < _currentDiagram.Devices.Count)
{
var conn = new Connection {
StartDevice = _currentDiagram.Devices[cModel.StartIndex],
EndDevice = _currentDiagram.Devices[cModel.EndIndex],
Type = cModel.Type
};
_currentDiagram.Connections.Add(conn);
RenderConnection(conn);
}
}
}
// Wait for Avalonia to measure/arrange the newly added controls
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Loaded);
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Render);
} catch (Exception ex) {
Console.WriteLine("Load failed: " + ex.Message);
}
}
}
private void WireTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wire;
private void WifiTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wifi;
private void AddText_Click(object? sender, RoutedEventArgs e)
{
var center = GetVisibleCanvasCenter();
AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, center.X - 60, center.Y - 40);
}
private async void ExportPng_Click(object? sender, RoutedEventArgs e)
{
if (_currentDiagram.Devices.Count == 0) return;
var prompt = new ExportOptionsWindow();
var result = await prompt.ShowDialog<ExportOption>(this);
if (result == ExportOption.Cancel) return;
var topLevel = GetTopLevel(this);
if (topLevel == null) return;
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Export PNG",
FileTypeChoices = new[] { new FilePickerFileType("PNG Image") { Patterns = new[] { "*.png" } } }
});
if (file != null)
{
double minX = _currentDiagram.Devices.Min(d => d.X) - 50;
double minY = _currentDiagram.Devices.Min(d => d.Y) - 50;
double maxX = _currentDiagram.Devices.Max(d => d.X) + 210;
double maxY = _currentDiagram.Devices.Max(d => d.Y) + 160;
int width = (int)(maxX - minX);
int height = (int)(maxY - minY);
if (width <= 0 || height <= 0) return;
// Create a temporary canvas for rendering to avoid live UI transform issues
var tempCanvas = new Canvas {
Width = width,
Height = height,
Background = (result == ExportOption.White || result == ExportOption.BlackAndWhite) ? Brushes.White : Brushes.Transparent
};
// Create a container to host temp canvas for rendering
var container = new Panel();
container.Children.Add(tempCanvas);
var rtb = new RenderTargetBitmap(new PixelSize(width, height), new Vector(96, 96));
// Fetch resources once for efficiency and consistency
var primaryBlue = Application.Current!.FindResource("PrimaryBlue") as IBrush;
var accentBlue = Application.Current!.FindResource("AccentBlue") as IBrush;
var textDark = Application.Current!.FindResource("TextDark") as IBrush ?? Brushes.Black;
var gridBlue = Application.Current!.FindResource("GridBlue") as IBrush;
bool isGrayscale = result == ExportOption.BlackAndWhite;
if (isGrayscale)
{
primaryBlue = Brushes.Black;
accentBlue = Brushes.Black;
textDark = Brushes.Black;
gridBlue = Brushes.Black;
}
// 1. Draw connections
foreach (var conn in _currentDiagram.Connections)
{
if (_connectionLines.TryGetValue(conn, out var line))
{
var tempLine = new Line {
Stroke = isGrayscale ? Brushes.Black : line.Stroke,
StrokeThickness = line.StrokeThickness,
StrokeDashArray = line.StrokeDashArray,
StartPoint = new Point(line.StartPoint.X - minX, line.StartPoint.Y - minY),
EndPoint = new Point(line.EndPoint.X - minX, line.EndPoint.Y - minY)
};
tempCanvas.Children.Add(tempLine);
}
}
// 2. Draw devices
foreach (var device in _currentDiagram.Devices)
{
if (_deviceElements.TryGetValue(device, out var original))
{
// Create a clone-like visual
var clone = new Border {
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12),
Background = Brushes.White,
BorderBrush = gridBlue ?? Brushes.Blue,
BorderThickness = new Thickness(isGrayscale ? 2 : 1),
MinWidth = 100,
MinHeight = 80,
MaxWidth = 180
};
var stack = new StackPanel { Spacing = 4 };
var img = new Image { Width = 64, Height = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
if (ImageConverter.Instance.Convert(device.IconPath, typeof(IImage), null, System.Globalization.CultureInfo.CurrentCulture) is IImage iimg)
{
img.Source = iimg;
}
var txt = new TextBlock {
Text = device.Name,
FontWeight = FontWeight.Bold,
FontSize = isGrayscale ? 14 : 12,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
Foreground = primaryBlue
};
stack.Children.Add(img);
stack.Children.Add(txt);
if (!string.IsNullOrWhiteSpace(device.Description))
{
var desc = new TextBlock {
Text = device.Description,
FontSize = isGrayscale ? 18 : 11,
FontWeight = isGrayscale ? FontWeight.Bold : FontWeight.Normal,
FontStyle = FontStyle.Italic,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
Foreground = textDark,
Opacity = isGrayscale ? 1.0 : 0.8
};
stack.Children.Add(desc);
}
foreach (var ip in device.IpAddresses)
{
var tb = new TextBlock {
Text = ip,
FontSize = isGrayscale ? 14 : 10,
FontWeight = isGrayscale ? FontWeight.Bold : FontWeight.Normal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
TextWrapping = TextWrapping.Wrap,
Foreground = accentBlue
};
stack.Children.Add(tb);
}
clone.Child = stack;
Canvas.SetLeft(clone, device.X - minX);
Canvas.SetTop(clone, device.Y - minY);
tempCanvas.Children.Add(clone);
}
}
// Important: Ensure layout of temp visual
container.Measure(new Size(width, height));
container.Arrange(new Rect(0, 0, width, height));
await Task.Delay(100);
rtb.Render(container);
if (result == ExportOption.BlackAndWhite)
{
using var ms = new MemoryStream();
rtb.Save(ms);
ms.Position = 0;
var wb = WriteableBitmap.Decode(ms);
ApplyGrayscale(wb);
using var stream = await file.OpenWriteAsync();
wb.Save(stream);
}
else
{
using var stream = await file.OpenWriteAsync();
rtb.Save(stream);
}
}
}
private void ApplyGrayscale(WriteableBitmap bitmap)
{
using (var lockedBitmap = bitmap.Lock())
{
int totalBytes = lockedBitmap.RowBytes * lockedBitmap.Size.Height;
byte[] buffer = new byte[totalBytes];
Marshal.Copy(lockedBitmap.Address, buffer, 0, totalBytes);
for (int y = 0; y < lockedBitmap.Size.Height; y++)
{
int rowOffset = y * lockedBitmap.RowBytes;
for (int x = 0; x < lockedBitmap.Size.Width; x++)
{
int i = rowOffset + x * 4;
if (i + 3 >= totalBytes) continue;
byte b = buffer[i];
byte g = buffer[i + 1];
byte r = buffer[i + 2];
byte a = buffer[i + 3];
if (a < 128)
{
// Transparent -> White
buffer[i] = 255;
buffer[i + 1] = 255;
buffer[i + 2] = 255;
}
else
{
// Standard luminance for grayscale
byte val = (byte)(0.299 * r + 0.587 * g + 0.114 * b);
buffer[i] = val;
buffer[i + 1] = val;
buffer[i + 2] = val;
}
buffer[i + 3] = 255; // Always opaque for printing
}
}
Marshal.Copy(buffer, 0, lockedBitmap.Address, totalBytes);
}
}
#endregion
}
}
-132
View File
@@ -1,132 +0,0 @@
<Window x:Class="NetworkDiagram.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
xmlns:local="clr-namespace:NetworkDiagram"
xmlns:models="clr-namespace:NetworkDiagram.Models"
mc:Ignorable="d"
Title="{DynamicResource AppTitle}" Height="700" Width="1080"
Background="#F5F9FF"
KeyDown="Window_KeyDown">
<Window.Resources>
<SolidColorBrush x:Key="HeaderBlue" Color="#004578"/>
<SolidColorBrush x:Key="AccentBlue" Color="#0078D7"/>
<SolidColorBrush x:Key="SidebarBlue" Color="#E1EEFA"/>
<SolidColorBrush x:Key="CanvasGridBlue" Color="#D0E2F2"/>
<Style x:Key="ModernButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource AccentBlue}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource HeaderBlue}" Padding="10">
<DockPanel>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<TextBlock Text="{DynamicResource AppTitle}" Foreground="White" FontSize="18" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,20,0"/>
<Button Content="{DynamicResource NewBtn}" Style="{StaticResource ModernButton}" Click="NewDiagram_Click"/>
<Button Content="{DynamicResource SaveBtn}" Style="{StaticResource ModernButton}" Click="SaveDiagram_Click"/>
<Button Content="{DynamicResource LoadBtn}" Style="{StaticResource ModernButton}" Click="LoadDiagram_Click"/>
<Button Content="{DynamicResource ExportBtn}" Style="{StaticResource ModernButton}" Click="ExportPng_Click"/>
<Separator Margin="10,0" Background="White" Opacity="0.5"/>
<Button Content="{DynamicResource WireBtn}" Style="{StaticResource ModernButton}" Click="WireTool_Click"/>
<Button Content="{DynamicResource WifiBtn}" Style="{StaticResource ModernButton}" Click="WifiTool_Click"/>
<Button Content="{DynamicResource AddTextBtn}" Style="{StaticResource ModernButton}" Click="AddText_Click"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<ComboBox x:Name="LangCombo" VerticalAlignment="Center" Width="80" SelectionChanged="LangCombo_SelectionChanged" Margin="10,0,0,0">
<ComboBoxItem Tag="en">EN</ComboBoxItem>
<ComboBoxItem Tag="cs" IsSelected="True">CZ</ComboBoxItem>
</ComboBox>
</StackPanel>
</DockPanel>
</Border>
<Border Grid.Row="1" Grid.Column="0" Background="{StaticResource SidebarBlue}" BorderBrush="{StaticResource CanvasGridBlue}" BorderThickness="0,0,1,0">
<DockPanel>
<TextBlock Text="{DynamicResource DevicesHeader}" DockPanel.Dock="Top" FontSize="16" FontWeight="SemiBold" Margin="10" Foreground="{StaticResource HeaderBlue}"/>
<ListBox x:Name="ToolboxList" Background="Transparent" BorderThickness="0" Margin="5"
PreviewMouseDown="Toolbox_PreviewMouseDown">
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="{StaticResource AccentBlue}" BorderThickness="1" CornerRadius="4" Margin="5" Padding="10" Background="White">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding IconPath}" Width="32" Height="32" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontWeight="Medium"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>
<ScrollViewer Grid.Row="1" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Canvas x:Name="DiagramCanvas" Width="2000" Height="2000" AllowDrop="True"
Drop="DiagramCanvas_Drop" DragOver="DiagramCanvas_DragOver"
MouseDown="DiagramCanvas_MouseDown"
MouseMove="DiagramCanvas_MouseMove"
MouseUp="DiagramCanvas_MouseUp">
<Canvas.Background>
<VisualBrush TileMode="Tile" Viewport="0,0,40,40" ViewportUnits="Absolute">
<VisualBrush.Visual>
<Path Data="M 0 40 L 0 0 L 40 0" Stroke="{StaticResource CanvasGridBlue}" StrokeThickness="0.5"/>
</VisualBrush.Visual>
</VisualBrush>
</Canvas.Background>
<Canvas.Resources>
<DataTemplate x:Key="DeviceTemplate">
<Border x:Name="DeviceBorder" BorderBrush="Transparent" BorderThickness="2" CornerRadius="4"
Background="Transparent" Padding="5" MaxWidth="150">
<StackPanel>
<Image Source="{Binding IconPath}" Width="48" Height="48" HorizontalAlignment="Center">
<Image.Style>
<Style TargetType="Image">
<Style.Triggers>
<Trigger Property="Source" Value="{x:Null}">
<Setter Property="Height" Value="0"/>
</Trigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<TextBlock Text="{Binding Name}" FontWeight="Bold" HorizontalAlignment="Center"
Foreground="#004578" TextWrapping="Wrap" TextAlignment="Center"/>
<ItemsControl ItemsSource="{Binding IpAddresses}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontSize="10" Foreground="#0078D7"
HorizontalAlignment="Center" TextWrapping="Wrap"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</DataTemplate>
</Canvas.Resources>
<!-- Selection rectangle visual -->
<Rectangle x:Name="SelectionRect" Stroke="{StaticResource AccentBlue}" StrokeDashArray="2,2"
Fill="#330078D7" Visibility="Collapsed" Panel.ZIndex="9999"/>
</Canvas>
</ScrollViewer>
</Grid>
</Window>
-554
View File
@@ -1,554 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Microsoft.Win32;
using NetworkDiagram.Models;
using Path = System.IO.Path;
namespace NetworkDiagram
{
public partial class MainWindow : Window
{
private List<DeviceTemplate> _deviceTemplates = new List<DeviceTemplate>();
private Diagram _currentDiagram = new Diagram();
// Selection & Dragging
private HashSet<FrameworkElement> _selectedElements = new HashSet<FrameworkElement>();
private Point _dragStartPoint;
private bool _isDraggingDevices;
private bool _isSelectingArea;
private Point _selectionStartPoint;
private ConnectionType? _activeTool;
private PlacedDevice? _firstDeviceForConnection;
private Dictionary<Connection, Line> _connectionLines = new Dictionary<Connection, Line>();
private Dictionary<PlacedDevice, FrameworkElement> _deviceElements = new Dictionary<PlacedDevice, FrameworkElement>();
public MainWindow()
{
InitializeComponent();
LoadTemplates();
}
private void LoadTemplates()
{
try
{
if (File.Exists("devices.json"))
{
string json = File.ReadAllText("devices.json");
_deviceTemplates = JsonSerializer.Deserialize<List<DeviceTemplate>>(json) ?? new List<DeviceTemplate>();
foreach(var t in _deviceTemplates)
{
if (!string.IsNullOrEmpty(t.IconPath)) t.IconPath = Path.GetFullPath(t.IconPath);
}
ToolboxList.ItemsSource = _deviceTemplates;
}
}
catch (Exception ex) { MessageBox.Show($"Error loading templates: {ex.Message}"); }
}
private string GetLocalizedString(string key) => Application.Current.Resources[key] as string ?? key;
private void LangCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (LangCombo.SelectedItem is ComboBoxItem item && item.Tag is string lang)
{
var dict = new ResourceDictionary();
dict.Source = new Uri($"Localization/Strings.{lang}.xaml", UriKind.Relative);
// Replace the existing localization dictionary
var oldDict = Application.Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Source != null && d.Source.OriginalString.Contains("Localization/Strings."));
if (oldDict != null) Application.Current.Resources.MergedDictionaries.Remove(oldDict);
Application.Current.Resources.MergedDictionaries.Add(dict);
}
}
#region Drag and Drop (Toolbox to Canvas)
private void Toolbox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (sender is ListBox listBox && listBox.SelectedItem is DeviceTemplate template)
DragDrop.DoDragDrop(listBox, template, DragDropEffects.Copy);
}
private void DiagramCanvas_DragOver(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(typeof(DeviceTemplate)) ? DragDropEffects.Copy : DragDropEffects.None;
e.Handled = true;
}
private void DiagramCanvas_Drop(object sender, DragEventArgs e)
{
if (e.Data.GetDataPresent(typeof(DeviceTemplate)))
{
var template = (DeviceTemplate)e.Data.GetData(typeof(DeviceTemplate));
Point dropPoint = e.GetPosition(DiagramCanvas);
AddDeviceToCanvas(template, dropPoint.X, dropPoint.Y);
}
}
#endregion
private void AddDeviceToCanvas(DeviceTemplate template, double x, double y)
{
var placed = new PlacedDevice { Name = template.Name, IconPath = template.IconPath, X = x, Y = y };
_currentDiagram.Devices.Add(placed);
RenderDevice(placed);
}
private void RenderDevice(PlacedDevice device)
{
var template = (DataTemplate)DiagramCanvas.Resources["DeviceTemplate"];
var contentPresenter = new ContentPresenter { Content = device, ContentTemplate = template };
var container = new Border { Child = contentPresenter, Tag = device, Background = Brushes.Transparent };
Canvas.SetLeft(container, device.X);
Canvas.SetTop(container, device.Y);
device.PropertyChanged += (s, e) => {
if (e.PropertyName == nameof(PlacedDevice.X)) Canvas.SetLeft(container, device.X);
if (e.PropertyName == nameof(PlacedDevice.Y)) Canvas.SetTop(container, device.Y);
};
container.MouseDown += Device_MouseDown;
container.MouseLeftButtonDown += (s, e) => {
if (e.ClickCount == 2) {
StopDragging();
EditDevice(device);
e.Handled = true;
}
};
_deviceElements[device] = container;
DiagramCanvas.Children.Add(container);
}
private void StopDragging()
{
_isDraggingDevices = false;
_isSelectingArea = false;
SelectionRect.Visibility = Visibility.Collapsed;
DiagramCanvas.ReleaseMouseCapture();
}
private void Device_MouseDown(object sender, MouseButtonEventArgs e)
{
if (sender is FrameworkElement element && element.Tag is PlacedDevice device)
{
if (_activeTool != null)
{
HandleConnectionTool(device, element);
e.Handled = true;
return;
}
// Handle Selection
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
{
if (!_selectedElements.Contains(element))
{
ClearSelection();
SelectElement(element);
}
}
else
{
if (_selectedElements.Contains(element)) DeselectElement(element);
else SelectElement(element);
}
// Start Dragging
_isDraggingDevices = true;
_dragStartPoint = e.GetPosition(DiagramCanvas);
DiagramCanvas.CaptureMouse();
e.Handled = true;
}
}
private void SelectElement(FrameworkElement element)
{
_selectedElements.Add(element);
element.Opacity = 0.7;
if (element is Line l) l.StrokeThickness = 5;
}
private void DeselectElement(FrameworkElement element)
{
_selectedElements.Remove(element);
element.Opacity = 1.0;
if (element is Line l) l.StrokeThickness = 3;
}
private void ClearSelection()
{
foreach (var el in _selectedElements.ToList()) DeselectElement(el);
}
#region Canvas Interaction (Selection Area & Dragging)
private void DiagramCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource == DiagramCanvas)
{
ClearSelection();
_isSelectingArea = true;
_selectionStartPoint = e.GetPosition(DiagramCanvas);
Canvas.SetLeft(SelectionRect, _selectionStartPoint.X);
Canvas.SetTop(SelectionRect, _selectionStartPoint.Y);
SelectionRect.Width = 0;
SelectionRect.Height = 0;
SelectionRect.Visibility = Visibility.Visible;
DiagramCanvas.CaptureMouse();
}
}
private void DiagramCanvas_MouseMove(object sender, MouseEventArgs e)
{
Point currentPoint = e.GetPosition(DiagramCanvas);
if (_isSelectingArea)
{
double x = Math.Min(_selectionStartPoint.X, currentPoint.X);
double y = Math.Min(_selectionStartPoint.Y, currentPoint.Y);
double w = Math.Abs(_selectionStartPoint.X - currentPoint.X);
double h = Math.Abs(_selectionStartPoint.Y - currentPoint.Y);
Canvas.SetLeft(SelectionRect, x);
Canvas.SetTop(SelectionRect, y);
SelectionRect.Width = w;
SelectionRect.Height = h;
// Real-time selection preview
Rect selectionBounds = new Rect(x, y, w, h);
foreach (var border in _deviceElements.Values)
{
Rect elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.ActualWidth, border.ActualHeight);
if (selectionBounds.IntersectsWith(elementBounds))
{
if (!_selectedElements.Contains(border)) SelectElement(border);
}
else
{
// Only deselect if we are in the middle of a selection area drag
if (_selectedElements.Contains(border)) DeselectElement(border);
}
}
// Also preview for lines
foreach (var line in _connectionLines.Values)
{
Rect lineBounds = new Rect(
Math.Min(line.X1, line.X2),
Math.Min(line.Y1, line.Y2),
Math.Abs(line.X1 - line.X2),
Math.Abs(line.Y1 - line.Y2));
if (selectionBounds.IntersectsWith(lineBounds))
{
if (!_selectedElements.Contains(line)) SelectElement(line);
}
else
{
if (_selectedElements.Contains(line)) DeselectElement(line);
}
}
}
else if (_isDraggingDevices && e.LeftButton == MouseButtonState.Pressed)
{
double deltaX = currentPoint.X - _dragStartPoint.X;
double deltaY = currentPoint.Y - _dragStartPoint.Y;
foreach (var element in _selectedElements)
{
if (element.Tag is PlacedDevice device)
{
device.X += deltaX;
device.Y += deltaY;
}
}
_dragStartPoint = currentPoint;
}
}
private void DiagramCanvas_MouseUp(object sender, MouseButtonEventArgs e)
{
if (_isSelectingArea)
{
Rect selectionBounds = new Rect(
Canvas.GetLeft(SelectionRect),
Canvas.GetTop(SelectionRect),
SelectionRect.Width,
SelectionRect.Height);
foreach (var border in _deviceElements.Values)
{
Rect elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.ActualWidth, border.ActualHeight);
if (selectionBounds.IntersectsWith(elementBounds)) SelectElement(border);
}
}
StopDragging();
}
#endregion
private void HandleConnectionTool(PlacedDevice device, FrameworkElement element)
{
if (_firstDeviceForConnection == null)
{
_firstDeviceForConnection = device;
element.Opacity = 0.5;
}
else
{
if (_firstDeviceForConnection != device)
{
var conn = new Connection { StartDevice = _firstDeviceForConnection, EndDevice = device, Type = _activeTool.Value };
_currentDiagram.Connections.Add(conn);
RenderConnection(conn);
}
ResetConnectionTool();
}
}
private void ResetConnectionTool()
{
_firstDeviceForConnection = null;
_activeTool = null;
foreach (var child in DiagramCanvas.Children.OfType<Border>()) child.Opacity = 1.0;
}
private void RenderConnection(Connection conn)
{
var line = new Line
{
Stroke = conn.Type == ConnectionType.Wifi ? Brushes.DeepSkyBlue : Brushes.SlateGray,
StrokeThickness = 3,
Tag = conn
};
if (conn.Type == ConnectionType.Wifi) line.StrokeDashArray = new DoubleCollection { 2, 2 };
UpdateLinePosition(conn, line);
conn.StartDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdateLinePosition(conn, line); };
conn.EndDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdateLinePosition(conn, line); };
line.MouseDown += (s, e) => {
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl)) ClearSelection();
SelectElement(line);
e.Handled = true;
};
_connectionLines[conn] = line;
DiagramCanvas.Children.Insert(0, line);
}
private void UpdateLinePosition(Connection conn, Line line)
{
if (_deviceElements.TryGetValue(conn.StartDevice, out var startElem) &&
_deviceElements.TryGetValue(conn.EndDevice, out var endElem))
{
if (startElem.ActualWidth == 0) startElem.UpdateLayout();
if (endElem.ActualWidth == 0) endElem.UpdateLayout();
line.X1 = conn.StartDevice.X + (startElem.ActualWidth / 2);
line.Y1 = conn.StartDevice.Y + 24;
line.X2 = conn.EndDevice.X + (endElem.ActualWidth / 2);
line.Y2 = conn.EndDevice.Y + 24;
}
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Delete && _selectedElements.Count > 0)
{
foreach (var el in _selectedElements.ToList())
{
if (el is Line line && el.Tag is Connection conn)
{
_currentDiagram.Connections.Remove(conn);
_connectionLines.Remove(conn);
DiagramCanvas.Children.Remove(line);
}
else if (el is Border border && border.Tag is PlacedDevice device)
{
_currentDiagram.Devices.Remove(device);
_deviceElements.Remove(device);
var toRemove = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
foreach (var c in toRemove)
{
if (_connectionLines.TryGetValue(c, out var l)) DiagramCanvas.Children.Remove(l);
_currentDiagram.Connections.Remove(c);
_connectionLines.Remove(c);
}
DiagramCanvas.Children.Remove(border);
}
}
_selectedElements.Clear();
}
else if (e.Key == Key.Escape) { ResetConnectionTool(); StopDragging(); ClearSelection(); }
}
private void EditDevice(PlacedDevice device)
{
var dialog = new EditDeviceWindow(device);
dialog.ShowDialog();
if (_deviceElements.TryGetValue(device, out var elem))
{
elem.UpdateLayout();
var attached = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
foreach(var c in attached) if (_connectionLines.TryGetValue(c, out var l)) UpdateLinePosition(c, l);
}
}
#region Toolbar Events
private void NewDiagram_Click(object sender, RoutedEventArgs e)
{
_currentDiagram = new Diagram();
DiagramCanvas.Children.Clear();
_connectionLines.Clear();
_deviceElements.Clear();
ClearSelection();
}
private void SaveDiagram_Click(object sender, RoutedEventArgs e)
{
var sfd = new SaveFileDialog { Filter = "Network Diagram (*.ndjson)|*.ndjson" };
if (sfd.ShowDialog() == true)
{
var model = new DiagramSaveModel {
Devices = _currentDiagram.Devices,
Connections = _currentDiagram.Connections.Select(c => new ConnectionSaveModel {
StartIndex = _currentDiagram.Devices.IndexOf(c.StartDevice),
EndIndex = _currentDiagram.Devices.IndexOf(c.EndDevice),
Type = c.Type
}).ToList()
};
string json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(sfd.FileName, json);
}
}
private void LoadDiagram_Click(object sender, RoutedEventArgs e)
{
var ofd = new OpenFileDialog { Filter = "Network Diagram (*.ndjson)|*.ndjson" };
if (ofd.ShowDialog() == true)
{
string json = File.ReadAllText(ofd.FileName);
var model = JsonSerializer.Deserialize<DiagramSaveModel>(json);
if (model == null) return;
_currentDiagram = new Diagram { Devices = model.Devices };
DiagramCanvas.Children.Clear();
_connectionLines.Clear();
_deviceElements.Clear();
ClearSelection();
foreach (var device in _currentDiagram.Devices) RenderDevice(device);
foreach (var cModel in model.Connections)
{
var conn = new Connection { StartDevice = _currentDiagram.Devices[cModel.StartIndex], EndDevice = _currentDiagram.Devices[cModel.EndIndex], Type = cModel.Type };
_currentDiagram.Connections.Add(conn);
RenderConnection(conn);
}
}
}
private void WireTool_Click(object sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wire;
private void WifiTool_Click(object sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wifi;
private void AddText_Click(object sender, RoutedEventArgs e) => AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, 100, 100);
private void ExportPng_Click(object sender, RoutedEventArgs e)
{
if (_currentDiagram.Devices.Count == 0)
{
MessageBox.Show(GetLocalizedString("EmptyDiagramMsg"));
return;
}
var sfd = new SaveFileDialog { Filter = "PNG Image (*.png)|*.png", Title = GetLocalizedString("ExportOptionsTitle") };
if (sfd.ShowDialog() != true) return;
var result = MessageBox.Show(GetLocalizedString("ExportWhiteBgMsg"), GetLocalizedString("ExportOptionsTitle"), MessageBoxButton.YesNoCancel);
if (result == MessageBoxResult.Cancel) return;
bool whiteBg = result == MessageBoxResult.Yes;
// 1. Find content bounds
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
foreach (var border in _deviceElements.Values)
{
double x = Canvas.GetLeft(border);
double y = Canvas.GetTop(border);
minX = Math.Min(minX, x);
minY = Math.Min(minY, y);
maxX = Math.Max(maxX, x + border.ActualWidth);
maxY = Math.Max(maxY, y + border.ActualHeight);
}
foreach (var line in _connectionLines.Values)
{
minX = Math.Min(minX, Math.Min(line.X1, line.X2));
minY = Math.Min(minY, Math.Min(line.Y1, line.Y2));
maxX = Math.Max(maxX, Math.Max(line.X1, line.X2));
maxY = Math.Max(maxY, Math.Max(line.Y1, line.Y2));
}
double margin = 20;
minX -= margin; minY -= margin; maxX += margin; maxY += margin;
double width = Math.Max(1, maxX - minX);
double height = Math.Max(1, maxY - minY);
try
{
RenderTargetBitmap rtb = new RenderTargetBitmap((int)width, (int)height, 96, 96, PixelFormats.Pbgra32);
DrawingVisual dv = new DrawingVisual();
using (DrawingContext dc = dv.RenderOpen())
{
if (whiteBg) dc.DrawRectangle(Brushes.White, null, new Rect(0, 0, width, height));
dc.PushTransform(new TranslateTransform(-minX, -minY));
foreach (var child in DiagramCanvas.Children)
{
if (child == SelectionRect || !(child is Visual v) || ((UIElement)child).Visibility != Visibility.Visible) continue;
double left = Canvas.GetLeft((UIElement)child);
double top = Canvas.GetTop((UIElement)child);
if (child is Line line)
{
dc.DrawLine(new Pen(line.Stroke, line.StrokeThickness) { DashStyle = new DashStyle(line.StrokeDashArray, 0) },
new Point(line.X1, line.Y1), new Point(line.X2, line.Y2));
}
else if (child is FrameworkElement fe)
{
VisualBrush vb = new VisualBrush(fe) { Stretch = Stretch.None };
dc.DrawRectangle(vb, null, new Rect(left, top, fe.ActualWidth, fe.ActualHeight));
}
}
}
rtb.Render(dv);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
using (var stream = File.Create(sfd.FileName)) encoder.Save(stream);
MessageBox.Show(GetLocalizedString("ExportSuccess"));
}
catch (Exception ex) { MessageBox.Show($"Export failed: {ex.Message}"); }
}
#endregion
}
public class DiagramSaveModel {
public List<PlacedDevice> Devices { get; set; } = new();
public List<ConnectionSaveModel> Connections { get; set; } = new();
}
public class ConnectionSaveModel {
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public ConnectionType Type { get; set; }
}
}
+115 -18
View File
@@ -1,6 +1,10 @@
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace NetworkDiagram.Models namespace NetworkDiagram.Models
{ {
@@ -12,54 +16,92 @@ namespace NetworkDiagram.Models
public class DeviceTemplate public class DeviceTemplate
{ {
public string Name { get; set; } = string.Empty; [JsonPropertyName("Name")]
public string Name { get; init; } = string.Empty;
[JsonPropertyName("IconPath")]
public string IconPath { get; set; } = string.Empty; public string IconPath { get; set; } = string.Empty;
} }
public class PlacedDevice : INotifyPropertyChanged public class PlacedDevice : INotifyPropertyChanged
{ {
private string _name = string.Empty; private string _name = string.Empty;
private List<string> _ipAddresses = new List<string>();
private string _annotation = string.Empty;
private double _x;
private double _y;
[JsonPropertyName("Name")]
public string Name public string Name
{ {
get => _name; get => _name;
set { _name = value; OnPropertyChanged(); } set
{
_name = value;
OnPropertyChanged();
}
} }
private string _description = string.Empty;
[JsonPropertyName("Description")]
public string Description
{
get => _description;
set
{
_description = value;
OnPropertyChanged();
}
}
[JsonPropertyName("TemplateName")]
public string TemplateName { get; set; } = string.Empty;
[JsonIgnore]
public string IconPath { get; set; } = string.Empty; public string IconPath { get; set; } = string.Empty;
private double _x;
[JsonPropertyName("X")]
public double X public double X
{ {
get => _x; get => _x;
set { _x = value; OnPropertyChanged(); OnPropertyChanged(nameof(CenterX)); } set
{
_x = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CenterX));
}
} }
private double _y;
[JsonPropertyName("Y")]
public double Y public double Y
{ {
get => _y; get => _y;
set { _y = value; OnPropertyChanged(); OnPropertyChanged(nameof(CenterY)); } set
{
_y = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CenterY));
}
} }
private List<string> _ipAddresses = [];
[JsonPropertyName("IpAddresses")]
public List<string> IpAddresses public List<string> IpAddresses
{ {
get => _ipAddresses; get => _ipAddresses;
set { _ipAddresses = value; OnPropertyChanged(); } set
} {
_ipAddresses = value;
public string Annotation OnPropertyChanged();
{ }
get => _annotation;
set { _annotation = value; OnPropertyChanged(); }
} }
[JsonIgnore]
public double CenterX => X + 50; public double CenterX => X + 50;
[JsonIgnore]
public double CenterY => Y + 40; public double CenterY => Y + 40;
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null) protected void OnPropertyChanged([CallerMemberName] string? name = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
@@ -75,7 +117,62 @@ namespace NetworkDiagram.Models
public class Diagram public class Diagram
{ {
public List<PlacedDevice> Devices { get; set; } = new List<PlacedDevice>(); public List<PlacedDevice> Devices { get; set; } = [];
public List<Connection> Connections { get; set; } = new List<Connection>(); public List<Connection> Connections { get; set; } = [];
}
public class ImageConverter : IValueConverter
{
public static ImageConverter Instance { get; } = new();
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is string path && !string.IsNullOrWhiteSpace(path))
{
try
{
if (path.StartsWith("avares://"))
return new Bitmap(AssetLoader.Open(new Uri(path)));
if (File.Exists(path))
return new Bitmap(path);
string uriPath = path.Replace("\\", "/");
if (!uriPath.StartsWith("Assets/")) uriPath = "Assets/" + uriPath.TrimStart('/');
string escapedPath = uriPath.Replace(" ", "%20");
var uri = new Uri($"avares://NetworkDiagram/{escapedPath}");
return new Bitmap(AssetLoader.Open(uri));
}
catch (Exception ex)
{
Console.WriteLine($"[ImageConverter] Error loading '{path}': {ex.Message}");
return null;
}
}
return null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
}
public class DiagramSaveModel {
[JsonPropertyName("Devices")]
public List<PlacedDevice> Devices { get; set; } = [];
[JsonPropertyName("Connections")]
public List<ConnectionSaveModel> Connections { get; set; } = [];
}
public class ConnectionSaveModel {
[JsonPropertyName("StartIndex")]
public int StartIndex { get; set; }
[JsonPropertyName("EndIndex")]
public int EndIndex { get; set; }
[JsonPropertyName("Type")]
public ConnectionType Type { get; set; }
} }
} }
+22 -16
View File
@@ -1,19 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<PropertyGroup> <ItemGroup>
<OutputType>WinExe</OutputType> <PackageReference Include="Avalonia" Version="11.0.10" />
<TargetFramework>net10.0-windows</TargetFramework> <PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
<Nullable>enable</Nullable> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
<ImplicitUsings>enable</ImplicitUsings> <PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.10" />
<UseWPF>true</UseWPF> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
</PropertyGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="devices.json"> <AvaloniaResource Include="Assets\**" />
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <AvaloniaResource Include="Localization\**" />
</None> <None Update="devices.json">
<None Update="Assets\**"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None>
</None> </ItemGroup>
</ItemGroup>
</Project> </Project>
+16
View File
@@ -0,0 +1,16 @@
using Avalonia;
namespace NetworkDiagram;
class Program
{
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
}
+8 -4
View File
@@ -8,12 +8,12 @@
"IconPath": "Assets/workgroup switch.jpg" "IconPath": "Assets/workgroup switch.jpg"
}, },
{ {
"Name": "Server", "Name": "L3 Switch",
"IconPath": "Assets/fileserver.jpg" "IconPath": "Assets/layer 3 switch.jpg"
}, },
{ {
"Name": "Storage server", "Name": "Server",
"IconPath": "Assets/storage server.jpg" "IconPath": "Assets/fileserver.jpg"
}, },
{ {
"Name": "Personal Computer", "Name": "Personal Computer",
@@ -54,5 +54,9 @@
{ {
"Name": "Wireless Router", "Name": "Wireless Router",
"IconPath": "Assets/wireless router.jpg" "IconPath": "Assets/wireless router.jpg"
},
{
"Name": "Video camera",
"IconPath": "Assets/video camera.jpg"
} }
] ]