Apply skill gradient Primary/Danger buttons to Elfisa.UI and add button demos.

Integrate winforms-modern-buttons gradient paint for primary actions and clear (×) controls, align PharmaTrust startup theme, and add Material/gradient/button skill demo projects for comparison.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Magomed 2026-07-14 14:27:31 +03:00
parent 14c4b08ad6
commit 1ccb2b33cf
21 changed files with 1566 additions and 111 deletions

View File

@ -1,23 +1,95 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elfisa.UI", "src\Elfisa.UI\Elfisa.UI.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ElectronicPharmacy", "src\ElectronicPharmacy\ElectronicPharmacy.csproj", "{145D8060-4187-4871-B4FB-8DF795727B66}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elfisa.MaterialDemo", "src\Elfisa.MaterialDemo\Elfisa.MaterialDemo.csproj", "{7974F646-2F23-46E3-A614-A3D0C5D8C844}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elfisa.ButtonSkillDemo", "src\Elfisa.ButtonSkillDemo\Elfisa.ButtonSkillDemo.csproj", "{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elfisa.GradientButtonDemo", "src\Elfisa.GradientButtonDemo\Elfisa.GradientButtonDemo.csproj", "{19457083-89B8-4C5B-BCA1-BFB66C273502}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Debug|Any CPU.Build.0 = Debug|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Debug|x64.ActiveCfg = Debug|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Debug|x64.Build.0 = Debug|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Debug|x86.ActiveCfg = Debug|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Debug|x86.Build.0 = Debug|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Release|Any CPU.ActiveCfg = Release|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Release|Any CPU.Build.0 = Release|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Release|x64.ActiveCfg = Release|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Release|x64.Build.0 = Release|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Release|x86.ActiveCfg = Release|Any CPU
{145D8060-4187-4871-B4FB-8DF795727B66}.Release|x86.Build.0 = Release|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Debug|x64.ActiveCfg = Debug|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Debug|x64.Build.0 = Debug|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Debug|x86.ActiveCfg = Debug|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Debug|x86.Build.0 = Debug|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Release|Any CPU.Build.0 = Release|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Release|x64.ActiveCfg = Release|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Release|x64.Build.0 = Release|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Release|x86.ActiveCfg = Release|Any CPU
{7974F646-2F23-46E3-A614-A3D0C5D8C844}.Release|x86.Build.0 = Release|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Debug|x64.ActiveCfg = Debug|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Debug|x64.Build.0 = Debug|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Debug|x86.ActiveCfg = Debug|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Debug|x86.Build.0 = Debug|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Release|Any CPU.Build.0 = Release|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Release|x64.ActiveCfg = Release|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Release|x64.Build.0 = Release|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Release|x86.ActiveCfg = Release|Any CPU
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85}.Release|x86.Build.0 = Release|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Debug|x64.ActiveCfg = Debug|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Debug|x64.Build.0 = Debug|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Debug|x86.ActiveCfg = Debug|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Debug|x86.Build.0 = Debug|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Release|Any CPU.Build.0 = Release|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Release|x64.ActiveCfg = Release|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Release|x64.Build.0 = Release|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Release|x86.ActiveCfg = Release|Any CPU
{19457083-89B8-4C5B-BCA1-BFB66C273502}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7974F646-2F23-46E3-A614-A3D0C5D8C844} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{1A28299F-5E9B-4E2E-AEC1-EB35FB911F85} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{19457083-89B8-4C5B-BCA1-BFB66C273502} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Windows.Forms;
using Elfisa.UI.Theming;
@ -11,7 +11,7 @@ namespace Электронная_Фармация
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
ThemeManager.CurrentVariant = ThemeVariant.FluentWin11;
ThemeManager.CurrentVariant = ThemeVariant.PharmaTrust;
Application.Run(new ElectroPharmacy());
}
}

View File

@ -91,7 +91,7 @@ namespace Электронная_Фармация.UserControls
const int row1Y = 10;
const int controlHeight = 32;
const int filterHeight = 33;
const int buttonHeight = 34;
const int buttonHeight = 37;
const int rowGap = 14;
const int row2Y = row1Y + buttonHeight + rowGap;
const int commentWidth = 200;
@ -109,10 +109,10 @@ namespace Электронная_Фармация.UserControls
int right = panelToolbar.ClientSize.Width - pad;
btnDeleteOrder.SetBounds(right - 130, row1Y, 130, buttonHeight);
btnDeleteOrder.SetBounds(right - 148, row1Y, 148, buttonHeight);
right = btnDeleteOrder.Left - gap;
_btnSendOrder.SetBounds(right - 150, row1Y, 150, buttonHeight);
_btnSendOrder.SetBounds(right - 168, row1Y, 168, buttonHeight);
right = _btnSendOrder.Left - gap;
_comboConsignees.SetBounds(right - 190, filterY, 190, filterHeight);
@ -126,14 +126,15 @@ namespace Электронная_Фармация.UserControls
x = pad;
txtGoodFromOrders.SetBounds(x, row2Y, 220, controlHeight);
x = txtGoodFromOrders.Right + gap;
btnClearGoodSearch.SetBounds(x, row2Y, 34, controlHeight);
btnClearGoodSearch.SetBounds(x, row2Y, 32, controlHeight + 3);
x = btnClearGoodSearch.Right + gap;
txtOrderNumberFilter.SetBounds(x, row2Y, 220, controlHeight);
x = txtOrderNumberFilter.Right + gap;
btnClearOrderNumberFilter.SetBounds(x, row2Y, 34, controlHeight);
btnClearOrderNumberFilter.SetBounds(x, row2Y, 32, controlHeight + 3);
x = btnClearOrderNumberFilter.Right + gap;
btnAddComment.SetBounds(x, row2Y, commentWidth, buttonHeight);
ModernButtonStyles.FitToText(btnAddComment, buttonHeight);
btnClearSuppliers.Visible = false;

View File

@ -52,9 +52,9 @@ namespace Электронная_Фармация.UserControls
Tag = "chrome-child";
btnDeleteGood.Height = 34;
btnClearSearchInPriceList.Height = 32;
btnClearSearchInPriceList.Height = 35;
txtSearchGoodNameInPriceList.Height = 32;
btnClearSupplierFilter.Height = 32;
btnClearSupplierFilter.Height = 35;
panel1.SizeChanged += (_, __) => LayoutMarkupRow();
LayoutMarkupRow();

View File

@ -0,0 +1,223 @@
using System.Drawing;
using System.Windows.Forms;
namespace Elfisa.ButtonSkillDemo;
internal sealed class ButtonSkillDemoForm : Form
{
public ButtonSkillDemoForm()
{
Text = "Skill ModernButton — демо стилей";
Size = new Size(920, 620);
MinimumSize = new Size(820, 520);
StartPosition = FormStartPosition.CenterScreen;
BackColor = Color.FromArgb(18, 18, 22);
Font = new Font("Segoe UI", 10f);
var header = new Label
{
Text = "Custom Modern Button (skill) — варианты стилей",
ForeColor = Color.White,
AutoSize = false,
Height = 48,
Dock = DockStyle.Top,
Padding = new Padding(24, 14, 24, 0),
Font = new Font("Segoe UI Semibold", 14f, FontStyle.Bold)
};
var scroll = new Panel
{
Dock = DockStyle.Fill,
AutoScroll = true,
Padding = new Padding(24)
};
var layout = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 2,
Padding = new Padding(0, 8, 0, 24)
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 220));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
AddSection(layout, "Default (тёмная)", CreateDefaultButton());
AddSection(layout, "Светлая тема", CreateLightButton());
AddSection(layout, "Неоновый стиль", CreateNeonButton());
AddSection(layout, "Glass (полупрозрачный)", CreateGlassButton());
AddSection(layout, "Pharma Trust — синяя", CreatePharmaPrimaryButton());
AddSection(layout, "Pharma Trust — красный ×", CreatePharmaClearButton());
AddSection(layout, "Большая CTA", CreateLargeCtaButton());
var note = new Label
{
AutoSize = true,
MaximumSize = new Size(820, 0),
ForeColor = Color.FromArgb(180, 180, 190),
Text =
"Это отдельный класс SkillModernButton для демо. Основное приложение использует Elfisa.UI.Controls.ModernButton + ThemeManager.\n" +
"Наведите курсор и нажмите кнопки — видны hover и pressed.",
Margin = new Padding(0, 8, 0, 0)
};
layout.Controls.Add(note, 0, layout.RowCount);
layout.SetColumnSpan(note, 2);
scroll.Controls.Add(layout);
Controls.Add(scroll);
Controls.Add(header);
}
private static void AddSection(TableLayoutPanel layout, string title, Control content)
{
var row = layout.RowCount;
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var label = new Label
{
Text = title,
ForeColor = Color.FromArgb(200, 200, 210),
AutoSize = true,
Margin = new Padding(0, 16, 12, 8),
Font = new Font("Segoe UI Semibold", 10.5f, FontStyle.Bold)
};
content.Margin = new Padding(0, 12, 0, 8);
layout.Controls.Add(label, 0, row);
layout.Controls.Add(content, 1, row);
layout.RowCount++;
}
private static FlowLayoutPanel Row(params Control[] buttons)
{
var panel = new FlowLayoutPanel
{
AutoSize = true,
WrapContents = true,
FlowDirection = FlowDirection.LeftToRight
};
foreach (var btn in buttons)
{
panel.Controls.Add(btn);
}
return panel;
}
private static SkillModernButton CreateDefaultButton()
{
return new SkillModernButton
{
Text = "Красивая кнопка",
Size = new Size(200, 48),
Margin = new Padding(0, 0, 12, 0)
};
}
private static SkillModernButton CreateLightButton()
{
return new SkillModernButton
{
Text = "Светлая кнопка",
Size = new Size(200, 48),
BackColor = Color.White,
ForeColor = Color.FromArgb(30, 30, 30),
BorderColor = Color.FromArgb(0, 122, 204),
HoverColor = Color.FromArgb(245, 249, 255),
PressedColor = Color.FromArgb(230, 240, 250),
BorderRadius = 12
};
}
private static SkillModernButton CreateNeonButton()
{
return new SkillModernButton
{
Text = "Неон",
Size = new Size(180, 48),
BackColor = Color.FromArgb(20, 24, 28),
ForeColor = Color.FromArgb(220, 255, 250),
BorderRadius = 8,
BorderColor = Color.FromArgb(0, 255, 200),
HoverColor = Color.FromArgb(50, 255, 220),
PressedColor = Color.FromArgb(0, 180, 140),
BorderSize = 2
};
}
private static SkillModernButton CreateGlassButton()
{
return new SkillModernButton
{
Text = "Glass",
Size = new Size(180, 48),
BackColor = Color.FromArgb(80, 80, 80),
ForeColor = Color.White,
BorderColor = Color.FromArgb(160, 160, 170),
HoverColor = Color.FromArgb(110, 110, 120),
PressedColor = Color.FromArgb(60, 60, 68),
BorderRadius = 16,
BorderSize = 1
};
}
private static SkillModernButton CreatePharmaPrimaryButton()
{
return new SkillModernButton
{
Text = "Сохранить заказ",
Size = new Size(200, 40),
BackColor = Color.FromArgb(3, 105, 161),
ForeColor = Color.White,
BorderColor = Color.FromArgb(3, 105, 161),
HoverColor = Color.FromArgb(7, 89, 133),
PressedColor = Color.FromArgb(12, 74, 110),
BorderRadius = 6,
BorderSize = 0,
Font = new Font("Segoe UI Semibold", 10f, FontStyle.Bold)
};
}
private static SkillModernButton CreatePharmaClearButton()
{
return new SkillModernButton
{
Text = "×",
Size = new Size(32, 32),
BackColor = Color.FromArgb(220, 38, 38),
ForeColor = Color.White,
BorderColor = Color.FromArgb(220, 38, 38),
HoverColor = Color.FromArgb(239, 68, 68),
PressedColor = Color.FromArgb(185, 28, 28),
BorderRadius = 6,
BorderSize = 0,
Font = new Font("Segoe UI Semibold", 13f, FontStyle.Bold)
};
}
private static Control CreateLargeCtaButton()
{
return Row(
new SkillModernButton
{
Text = "Загрузить на сервер",
Size = new Size(220, 52),
BorderRadius = 20,
BorderColor = Color.FromArgb(64, 158, 255),
BackColor = Color.FromArgb(30, 30, 30),
HoverColor = Color.FromArgb(0, 140, 230),
PressedColor = Color.FromArgb(0, 100, 180)
},
new SkillModernButton
{
Text = "Отмена",
Size = new Size(140, 52),
BackColor = Color.White,
ForeColor = Color.FromArgb(30, 30, 30),
BorderColor = Color.FromArgb(200, 200, 210),
HoverColor = Color.FromArgb(245, 245, 248),
PressedColor = Color.FromArgb(230, 230, 235),
BorderRadius = 20
});
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,11 @@
namespace Elfisa.ButtonSkillDemo;
internal static class Program
{
[STAThread]
private static void Main()
{
ApplicationConfiguration.Initialize();
Application.Run(new ButtonSkillDemoForm());
}
}

View File

@ -0,0 +1,24 @@
# Button Skill Demo
Демо кнопок из skill **Custom Modern Button** (`SkillModernButton`).
Не путать с `Elfisa.UI.Controls.ModernButton` — это другой класс для основного приложения.
## Запуск
```powershell
dotnet run --project src/Elfisa.ButtonSkillDemo/Elfisa.ButtonSkillDemo.csproj -c Release
```
Exe: `src/Elfisa.ButtonSkillDemo/bin/Release/net8.0-windows/Elfisa.ButtonSkillDemo.exe`
## Стили в демо
| Секция | Настройки |
|--------|-----------|
| Default | тёмный фон, синяя рамка, radius 12 |
| Светлая | белый фон, тёмный текст |
| Неон | cyan border + hover |
| Glass | серый полупрозрачный вид |
| Pharma Trust | синяя / красная × как в ЭльФиСА |
| CTA | большие кнопки radius 20 |

View File

@ -0,0 +1,132 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace Elfisa.ButtonSkillDemo;
/// <summary>
/// Кнопка из skill «Custom Modern Button» (отдельно от Elfisa.UI.ModernButton).
/// </summary>
public sealed class SkillModernButton : Button
{
public Color BorderColor { get; set; } = Color.FromArgb(0, 122, 204);
public Color HoverColor { get; set; } = Color.FromArgb(0, 140, 230);
public Color PressedColor { get; set; } = Color.FromArgb(0, 100, 180);
public int BorderRadius { get; set; } = 12;
public int BorderSize { get; set; } = 2;
private bool _isHover;
private bool _isPressed;
public SkillModernButton()
{
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
BackColor = Color.FromArgb(30, 30, 30);
ForeColor = Color.White;
Font = new Font("Segoe UI", 10.5f, FontStyle.Bold);
Cursor = Cursors.Hand;
Size = new Size(160, 45);
SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.OptimizedDoubleBuffer |
ControlStyles.ResizeRedraw, true);
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
var rect = new Rectangle(0, 0, Width - 1, Height - 1);
var currentColor = BackColor;
if (_isPressed)
{
currentColor = PressedColor;
}
else if (_isHover)
{
currentColor = HoverColor;
}
using (var path = GetRoundedPath(rect, BorderRadius))
{
using (var brush = new SolidBrush(currentColor))
{
g.FillPath(brush, path);
}
if (BorderSize > 0)
{
using (var pen = new Pen(BorderColor, BorderSize))
{
g.DrawPath(pen, path);
}
}
}
var flags = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine;
TextRenderer.DrawText(g, Text, Font, rect, ForeColor, flags);
}
private static GraphicsPath GetRoundedPath(Rectangle rect, int radius)
{
var path = new GraphicsPath();
var r = Math.Max(0, Math.Min(radius, Math.Min(rect.Width, rect.Height) / 2));
if (r == 0)
{
path.AddRectangle(rect);
return path;
}
var diameter = r * 2;
path.StartFigure();
path.AddArc(rect.X, rect.Y, diameter, diameter, 180, 90);
path.AddArc(rect.Right - diameter, rect.Y, diameter, diameter, 270, 90);
path.AddArc(rect.Right - diameter, rect.Bottom - diameter, diameter, diameter, 0, 90);
path.AddArc(rect.X, rect.Bottom - diameter, diameter, diameter, 90, 90);
path.CloseFigure();
return path;
}
protected override void OnMouseEnter(EventArgs e)
{
_isHover = true;
Invalidate();
base.OnMouseEnter(e);
}
protected override void OnMouseLeave(EventArgs e)
{
_isHover = false;
_isPressed = false;
Invalidate();
base.OnMouseLeave(e);
}
protected override void OnMouseDown(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
_isPressed = true;
Invalidate();
}
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
_isPressed = false;
Invalidate();
base.OnMouseUp(e);
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net472</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<LangVersion>latest</LangVersion>
<RootNamespace>Elfisa.GradientButtonDemo</RootNamespace>
<AssemblyName>Elfisa.GradientButtonDemo</AssemblyName>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,205 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using ModernControls;
namespace Elfisa.GradientButtonDemo
{
internal sealed class GradientButtonDemoForm : Form
{
public GradientButtonDemoForm()
{
Text = "winforms-modern-buttons skill — демо";
Size = new Size(980, 700);
MinimumSize = new Size(860, 600);
StartPosition = FormStartPosition.CenterScreen;
BackColor = Color.FromArgb(245, 247, 250);
Font = new Font("Segoe UI", 10f);
var header = new Label
{
Text = "ModernGradientButton — градиент + тень + скругление (.NET Framework 4.7.2)",
Dock = DockStyle.Top,
Height = 52,
Padding = new Padding(24, 16, 24, 0),
Font = new Font("Segoe UI Semibold", 13f, FontStyle.Bold),
ForeColor = Color.FromArgb(30, 41, 59)
};
var scroll = new Panel
{
Dock = DockStyle.Fill,
AutoScroll = true,
Padding = new Padding(24, 8, 24, 24)
};
var layout = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
ColumnCount = 2,
Width = 900
};
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 240));
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
AddRow(layout, "Primary (skill)", PresetButton(ModernGradientButton.Preset.Primary, "Сохранить"));
AddRow(layout, "Success", PresetButton(ModernGradientButton.Preset.Success, "Подтвердить"));
AddRow(layout, "Danger", PresetButton(ModernGradientButton.Preset.Danger, "Удалить"));
AddRow(layout, "Neutral", PresetButton(ModernGradientButton.Preset.Neutral, "Отмена"));
AddRow(layout, "Кастом (оранжевый)", CustomOrangeButton());
AddRow(layout, "Pharma Trust — синяя", PharmaPrimaryButton());
AddRow(layout, "Крестик × у поиска", ClearSearchButton());
AddRow(layout, "Toolbar (мелкая)", ToolbarRow());
var note = new Label
{
AutoSize = true,
MaximumSize = new Size(860, 0),
Margin = new Padding(0, 20, 0, 0),
ForeColor = Color.FromArgb(100, 116, 139),
Text =
"Skill: winforms-modern-buttons · Класс ModernControls.ModernGradientButton\n" +
"Наведите и нажмите — видны hover/pressed. Тень: ShadowDepth, градиент: GradientTop/Bottom.\n" +
"Основное приложение ЭльФиСА пока на Elfisa.UI.ModernButton — это отдельное демо для сравнения."
};
layout.Controls.Add(note, 0, layout.RowCount);
layout.SetColumnSpan(note, 2);
scroll.Controls.Add(layout);
Controls.Add(scroll);
Controls.Add(header);
}
private static void AddRow(TableLayoutPanel layout, string title, Control content)
{
var row = layout.RowCount;
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
layout.Controls.Add(new Label
{
Text = title,
AutoSize = true,
Margin = new Padding(0, 18, 12, 8),
Font = new Font("Segoe UI Semibold", 10f, FontStyle.Bold),
ForeColor = Color.FromArgb(51, 65, 85)
}, 0, row);
content.Margin = new Padding(0, 14, 0, 4);
layout.Controls.Add(content, 1, row);
layout.RowCount++;
}
private static ModernGradientButton PresetButton(ModernGradientButton.Preset preset, string text)
{
var btn = new ModernGradientButton
{
Text = text,
Size = new Size(160, 48),
Margin = new Padding(0, 0, 12, 8)
};
btn.ApplyPreset(preset);
btn.Click += (_, __) => MessageBox.Show(text + " — клик!", "Демо", MessageBoxButtons.OK, MessageBoxIcon.Information);
return btn;
}
private static ModernGradientButton CustomOrangeButton()
{
return new ModernGradientButton
{
Text = "Свой стиль",
Size = new Size(180, 50),
CornerRadius = 20,
ShadowDepth = 6,
GradientTop = Color.FromArgb(255, 179, 71),
GradientBottom = Color.FromArgb(230, 126, 34),
HoverGradientTop = Color.FromArgb(255, 195, 110),
HoverGradientBottom = Color.FromArgb(245, 145, 50),
PressedGradientTop = Color.FromArgb(220, 120, 30),
PressedGradientBottom = Color.FromArgb(190, 100, 20)
};
}
private static ModernGradientButton PharmaPrimaryButton()
{
var btn = new ModernGradientButton
{
Text = "Сохранить заказ",
Size = new Size(190, 44),
CornerRadius = 6,
ShadowDepth = 4,
GradientTop = Color.FromArgb(14, 116, 181),
GradientBottom = Color.FromArgb(3, 105, 161),
HoverGradientTop = Color.FromArgb(37, 130, 195),
HoverGradientBottom = Color.FromArgb(7, 89, 133),
PressedGradientTop = Color.FromArgb(12, 74, 110),
PressedGradientBottom = Color.FromArgb(8, 60, 90)
};
return btn;
}
private static Control ClearSearchRow()
{
var panel = new FlowLayoutPanel { AutoSize = true, WrapContents = false };
var search = new TextBox
{
Text = "прокладки",
Width = 280,
Height = 32,
Font = new Font("Segoe UI", 10f),
Margin = new Padding(0, 8, 8, 0)
};
var clear = new ModernGradientButton
{
Text = "×",
Size = new Size(36, 36),
CornerRadius = 6,
ShadowDepth = 3,
Font = new Font("Segoe UI Semibold", 13f, FontStyle.Bold),
Margin = new Padding(0, 6, 0, 0)
};
clear.ApplyPreset(ModernGradientButton.Preset.Danger);
clear.Click += (_, __) => search.Clear();
panel.Controls.Add(search);
panel.Controls.Add(clear);
return panel;
}
private static Control ClearSearchButton()
{
return ClearSearchRow();
}
private static Control ToolbarRow()
{
var panel = new FlowLayoutPanel { AutoSize = true };
var small = new ModernGradientButton
{
Text = "×",
Size = new Size(32, 32),
CornerRadius = 5,
ShadowDepth = 2,
Font = new Font("Segoe UI", 11f, FontStyle.Bold)
};
small.ApplyPreset(ModernGradientButton.Preset.Danger);
var neutral = new ModernGradientButton
{
Text = "Фильтр",
Size = new Size(100, 34),
CornerRadius = 8,
ShadowDepth = 3
};
neutral.ApplyPreset(ModernGradientButton.Preset.Neutral);
panel.Controls.Add(small);
panel.Controls.Add(neutral);
return panel;
}
}
}

View File

@ -0,0 +1,220 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
namespace ModernControls
{
/// <summary>
/// Из skill winforms-modern-buttons: градиент, тень, скругление, hover/pressed.
/// </summary>
public class ModernGradientButton : Button
{
public enum ButtonState { Normal, Hover, Pressed }
private ButtonState _state = ButtonState.Normal;
[Category("Appearance")]
public Color GradientTop { get; set; } = Color.FromArgb(66, 133, 244);
[Category("Appearance")]
public Color GradientBottom { get; set; } = Color.FromArgb(30, 87, 194);
[Category("Appearance")]
public Color HoverGradientTop { get; set; } = Color.FromArgb(90, 150, 255);
[Category("Appearance")]
public Color HoverGradientBottom { get; set; } = Color.FromArgb(45, 105, 220);
[Category("Appearance")]
public Color PressedGradientTop { get; set; } = Color.FromArgb(25, 70, 160);
[Category("Appearance")]
public Color PressedGradientBottom { get; set; } = Color.FromArgb(15, 55, 130);
[Category("Appearance")]
public int CornerRadius { get; set; } = 12;
[Category("Appearance")]
public int ShadowDepth { get; set; } = 5;
[Category("Appearance")]
public Color ShadowColor { get; set; } = Color.FromArgb(60, 0, 0, 0);
[Category("Appearance")]
public bool AnimateHover { get; set; } = true;
public ModernGradientButton()
{
SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.UserPaint
| ControlStyles.ResizeRedraw
| ControlStyles.SupportsTransparentBackColor
| ControlStyles.OptimizedDoubleBuffer, true);
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
BackColor = Color.Transparent;
ForeColor = Color.White;
Font = new Font("Segoe UI", 9.5f, FontStyle.Regular);
Cursor = Cursors.Hand;
Size = new Size(140, 42);
Padding = new Padding(0, 0, 0, ShadowDepth);
}
protected override void OnPaintBackground(PaintEventArgs pevent)
{
}
protected override void OnMouseEnter(EventArgs e)
{
base.OnMouseEnter(e);
_state = ButtonState.Hover;
Invalidate();
}
protected override void OnMouseLeave(EventArgs e)
{
base.OnMouseLeave(e);
_state = ButtonState.Normal;
Invalidate();
}
protected override void OnMouseDown(MouseEventArgs mevent)
{
base.OnMouseDown(mevent);
_state = ButtonState.Pressed;
Invalidate();
}
protected override void OnMouseUp(MouseEventArgs mevent)
{
base.OnMouseUp(mevent);
_state = ClientRectangle.Contains(mevent.Location) ? ButtonState.Hover : ButtonState.Normal;
Invalidate();
}
private static GraphicsPath RoundedRect(Rectangle bounds, int radius)
{
var d = radius * 2;
var path = new GraphicsPath();
if (radius <= 0)
{
path.AddRectangle(bounds);
return path;
}
path.StartFigure();
path.AddArc(bounds.X, bounds.Y, d, d, 180, 90);
path.AddArc(bounds.Right - d, bounds.Y, d, d, 270, 90);
path.AddArc(bounds.Right - d, bounds.Bottom - d, d, d, 0, 90);
path.AddArc(bounds.X, bounds.Bottom - d, d, d, 90, 90);
path.CloseFigure();
return path;
}
protected override void OnPaint(PaintEventArgs pevent)
{
var g = pevent.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
var buttonRect = new Rectangle(0, 0, Width - 1, Height - 1 - ShadowDepth);
for (var i = ShadowDepth; i > 0; i--)
{
var alpha = ShadowColor.A * i / (ShadowDepth * 3);
using (var shadowBrush = new SolidBrush(Color.FromArgb(alpha, ShadowColor)))
using (var shadowPath = RoundedRect(
new Rectangle(i, buttonRect.Y + i + 1, buttonRect.Width - i, buttonRect.Height - 1),
CornerRadius))
{
g.FillPath(shadowBrush, shadowPath);
}
}
Color top;
Color bottom;
switch (_state)
{
case ButtonState.Hover:
top = HoverGradientTop;
bottom = HoverGradientBottom;
break;
case ButtonState.Pressed:
top = PressedGradientTop;
bottom = PressedGradientBottom;
break;
default:
top = GradientTop;
bottom = GradientBottom;
break;
}
using (var path = RoundedRect(buttonRect, CornerRadius))
using (var brush = new LinearGradientBrush(buttonRect, top, bottom, LinearGradientMode.Vertical))
{
g.FillPath(brush, path);
using (var highlightPen = new Pen(Color.FromArgb(40, Color.White), 1))
{
g.DrawArc(highlightPen, buttonRect.X, buttonRect.Y, CornerRadius * 2, CornerRadius * 2, 180, 90);
}
}
TextRenderer.DrawText(
g,
Text,
Font,
buttonRect,
ForeColor,
TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
}
public void ApplyPreset(Preset preset)
{
switch (preset)
{
case Preset.Primary:
GradientTop = Color.FromArgb(66, 133, 244);
GradientBottom = Color.FromArgb(30, 87, 194);
HoverGradientTop = Color.FromArgb(90, 150, 255);
HoverGradientBottom = Color.FromArgb(45, 105, 220);
PressedGradientTop = Color.FromArgb(25, 70, 160);
PressedGradientBottom = Color.FromArgb(15, 55, 130);
break;
case Preset.Success:
GradientTop = Color.FromArgb(67, 189, 118);
GradientBottom = Color.FromArgb(30, 140, 80);
HoverGradientTop = Color.FromArgb(90, 205, 135);
HoverGradientBottom = Color.FromArgb(40, 160, 95);
PressedGradientTop = Color.FromArgb(20, 120, 65);
PressedGradientBottom = Color.FromArgb(10, 95, 50);
break;
case Preset.Danger:
GradientTop = Color.FromArgb(235, 87, 87);
GradientBottom = Color.FromArgb(200, 40, 40);
HoverGradientTop = Color.FromArgb(245, 110, 110);
HoverGradientBottom = Color.FromArgb(215, 55, 55);
PressedGradientTop = Color.FromArgb(175, 30, 30);
PressedGradientBottom = Color.FromArgb(140, 20, 20);
break;
case Preset.Neutral:
GradientTop = Color.FromArgb(120, 130, 140);
GradientBottom = Color.FromArgb(80, 90, 100);
HoverGradientTop = Color.FromArgb(140, 150, 160);
HoverGradientBottom = Color.FromArgb(95, 105, 115);
PressedGradientTop = Color.FromArgb(65, 75, 85);
PressedGradientBottom = Color.FromArgb(50, 60, 70);
break;
}
Invalidate();
}
public enum Preset { Primary, Success, Danger, Neutral }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Windows.Forms;
namespace Elfisa.GradientButtonDemo
{
internal static class Program
{
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new GradientButtonDemoForm());
}
}
}

View File

@ -0,0 +1,20 @@
# Gradient Button Demo (skill)
Демо по skill **winforms-modern-buttons** — класс `ModernControls.ModernGradientButton`.
- .NET Framework **4.7.2** (как в skill и в ЭльФиСА)
- Градиент, тень, скругление, 4 пресета
## Запуск
```powershell
dotnet run --project src/Elfisa.GradientButtonDemo/Elfisa.GradientButtonDemo.csproj -c Release
```
Exe: `src/Elfisa.GradientButtonDemo/bin/Release/Elfisa.GradientButtonDemo.exe`
## Файлы skill
Исходник skill распакован из `winforms-modern-buttons.skill`:
- `ModernGradientButton.cs` — копия из `assets/`
- Skill также можно установить в `~/.cursor/skills/winforms-modern-buttons/`

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MaterialSkin.2" Version="2.3.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,286 @@
using MaterialSkin;
using MaterialSkin.Controls;
namespace Elfisa.MaterialDemo;
/// <summary>
/// Demo: как может выглядеть «Электронная Фармация» на MaterialSkin.2 (отдельно от Elfisa.UI).
/// </summary>
internal sealed class PharmacyMaterialForm : MaterialForm
{
private readonly MaterialTabControl _tabs;
private readonly Panel _contentHost;
private readonly MaterialLabel _statusLabel;
public PharmacyMaterialForm()
{
Text = "ЭльФиСА — MaterialSkin demo";
MinimumSize = new Size(960, 640);
Size = new Size(1100, 720);
StartPosition = FormStartPosition.CenterScreen;
MaterialSkinManager.Instance.AddFormToManage(this);
var header = new MaterialLabel
{
Text = "MaterialSkin.2 — демо для сравнения с Elfisa.UI",
FontType = MaterialSkinManager.fontType.H6,
Dock = DockStyle.Top,
Height = 48,
Padding = new Padding(24, 12, 24, 0)
};
_tabs = new MaterialTabControl
{
Dock = DockStyle.Top,
Height = 48,
Depth = 0
};
_tabs.TabPages.Add(new TabPage("Прайс-лист") { Tag = "price" });
_tabs.TabPages.Add(new TabPage("Заказы") { Tag = "orders" });
_tabs.TabPages.Add(new TabPage("Загрузить") { Tag = "upload" });
_tabs.SelectedIndexChanged += (_, __) => ShowTab(_tabs.SelectedTab?.Tag as string ?? "price");
_contentHost = new Panel
{
Dock = DockStyle.Fill,
Padding = new Padding(24)
};
_statusLabel = new MaterialLabel
{
Dock = DockStyle.Bottom,
Height = 36,
Text = "Пользователь: demo@apteka (MaterialSkin dark / teal)",
FontType = MaterialSkinManager.fontType.Caption,
Padding = new Padding(24, 8, 24, 8)
};
Controls.Add(_contentHost);
Controls.Add(_statusLabel);
Controls.Add(_tabs);
Controls.Add(header);
ShowTab("price");
}
private void ShowTab(string key)
{
_contentHost.Controls.Clear();
switch (key)
{
case "orders":
_contentHost.Controls.Add(BuildOrdersView());
break;
case "upload":
_contentHost.Controls.Add(BuildUploadView());
break;
default:
_contentHost.Controls.Add(BuildPriceView());
break;
}
}
private Control BuildPriceView()
{
var root = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 3
};
root.RowStyles.Add(new RowStyle(SizeType.AutoSize));
root.RowStyles.Add(new RowStyle(SizeType.Percent, 100f));
root.RowStyles.Add(new RowStyle(SizeType.Absolute, 120f));
var toolbar = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
AutoSize = true,
WrapContents = false,
Padding = new Padding(0, 0, 0, 16)
};
var search = new MaterialTextBox
{
Hint = "Поиск по наименованию…",
Width = 320,
Margin = new Padding(0, 8, 12, 0)
};
var clearBtn = new MaterialButton
{
Text = "×",
Type = MaterialButton.MaterialButtonType.Outlined,
Size = new Size(48, 36),
Margin = new Padding(0, 8, 12, 0)
};
var supplier = new MaterialComboBox
{
Hint = "Поставщик",
Width = 200,
Margin = new Padding(0, 8, 0, 0)
};
supplier.Items.AddRange(new object[] { "Катрен", "Протек", "Все поставщики" });
supplier.SelectedIndex = 0;
toolbar.Controls.Add(search);
toolbar.Controls.Add(clearBtn);
toolbar.Controls.Add(supplier);
var grid = new MaterialListView
{
Dock = DockStyle.Fill,
FullRowSelect = true,
View = View.Details,
BorderStyle = BorderStyle.None
};
grid.Columns.Add("Наименование", 420);
grid.Columns.Add("Производитель", 220);
grid.Columns.Add("Цена", 90);
grid.Columns.Add("Остаток", 90);
grid.Items.Add(new ListViewItem(new[] { "Нурофен 200 мг №10", "Reckitt", "189,00", "42" }));
grid.Items.Add(new ListViewItem(new[] { "Амоксициллин 500 мг №20", "Sandoz", "124,50", "18" }));
grid.Items.Add(new ListViewItem(new[] { "Омез 20 мг №30", "Dr. Reddy's", "312,00", "7" }));
var cartCard = new MaterialCard
{
Dock = DockStyle.Fill,
Padding = new Padding(16)
};
var cartLayout = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 4
};
cartLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40f));
cartLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20f));
cartLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20f));
cartLayout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20f));
cartLayout.Controls.Add(new MaterialLabel
{
Text = "Корзина: 2 позиции · 313,50 ₽",
FontType = MaterialSkinManager.fontType.Subtitle1,
Dock = DockStyle.Fill
}, 0, 0);
cartLayout.Controls.Add(new MaterialButton
{
Text = "Сохранить заказ",
Type = MaterialButton.MaterialButtonType.Contained,
Dock = DockStyle.Fill,
HighEmphasis = true
}, 1, 0);
cartLayout.Controls.Add(new MaterialButton
{
Text = "Очистить",
Type = MaterialButton.MaterialButtonType.Outlined,
Dock = DockStyle.Fill
}, 2, 0);
cartLayout.Controls.Add(new MaterialButton
{
Text = "Отправить",
Type = MaterialButton.MaterialButtonType.Contained,
Dock = DockStyle.Fill
}, 3, 0);
cartCard.Controls.Add(cartLayout);
root.Controls.Add(toolbar, 0, 0);
root.Controls.Add(grid, 0, 1);
root.Controls.Add(cartCard, 0, 2);
return root;
}
private Control BuildOrdersView()
{
var card = new MaterialCard
{
Dock = DockStyle.Fill,
Padding = new Padding(24)
};
var title = new MaterialLabel
{
Text = "Журнал заказов",
FontType = MaterialSkinManager.fontType.H5,
Dock = DockStyle.Top,
Height = 40
};
var grid = new MaterialListView
{
Dock = DockStyle.Fill,
View = View.Details,
FullRowSelect = true
};
grid.Columns.Add("№", 80);
grid.Columns.Add("Дата", 120);
grid.Columns.Add("Поставщик", 180);
grid.Columns.Add("Статус", 140);
grid.Columns.Add("Сумма", 100);
grid.Items.Add(new ListViewItem(new[] { "1042", "14.07.2026", "Катрен", "Отправлен", "4 820,00" }));
grid.Items.Add(new ListViewItem(new[] { "1041", "13.07.2026", "Протек", "Черновик", "1 240,00" }));
card.Controls.Add(grid);
card.Controls.Add(title);
return card;
}
private Control BuildUploadView()
{
var card = new MaterialCard
{
Dock = DockStyle.Fill,
Padding = new Padding(32)
};
var stack = new TableLayoutPanel
{
Dock = DockStyle.Fill,
ColumnCount = 1,
RowCount = 4
};
stack.RowStyles.Add(new RowStyle(SizeType.AutoSize));
stack.RowStyles.Add(new RowStyle(SizeType.AutoSize));
stack.RowStyles.Add(new RowStyle(SizeType.AutoSize));
stack.RowStyles.Add(new RowStyle(SizeType.Percent, 100f));
stack.Controls.Add(new MaterialLabel
{
Text = "Загрузка данных с сервера",
FontType = MaterialSkinManager.fontType.H5,
AutoSize = true,
Margin = new Padding(0, 0, 0, 8)
}, 0, 0);
stack.Controls.Add(new MaterialLabel
{
Text = "MaterialSkin даёт готовые карточки, ripple-кнопки и тёмную тему из коробки.",
FontType = MaterialSkinManager.fontType.Body1,
AutoSize = true,
Margin = new Padding(0, 0, 0, 24)
}, 0, 1);
var buttons = new FlowLayoutPanel { AutoSize = true, WrapContents = false };
buttons.Controls.Add(new MaterialButton
{
Text = "Загрузить прайс",
Type = MaterialButton.MaterialButtonType.Contained,
HighEmphasis = true,
Margin = new Padding(0, 0, 12, 0)
});
buttons.Controls.Add(new MaterialButton
{
Text = "Отправить заказы",
Type = MaterialButton.MaterialButtonType.Outlined
});
stack.Controls.Add(buttons, 0, 2);
card.Controls.Add(stack);
return card;
}
}

View File

@ -0,0 +1,24 @@
using MaterialSkin;
using MaterialSkin.Controls;
namespace Elfisa.MaterialDemo;
internal static class Program
{
[STAThread]
private static void Main()
{
ApplicationConfiguration.Initialize();
var skin = MaterialSkinManager.Instance;
skin.Theme = MaterialSkinManager.Themes.DARK;
skin.ColorScheme = new ColorScheme(
Primary.Teal800,
Primary.Teal900,
Primary.Teal200,
Accent.LightBlue200,
TextShade.WHITE);
Application.Run(new PharmacyMaterialForm());
}
}

View File

@ -0,0 +1,19 @@
# MaterialSkin demo
Отдельное приложение для сравнения **MaterialSkin.2** с текущей библиотекой **Elfisa.UI**.
Запуск:
```powershell
dotnet run --project src/Elfisa.MaterialDemo/Elfisa.MaterialDemo.csproj -c Release
```
Или exe:
`src/Elfisa.MaterialDemo/bin/Release/net8.0-windows/Elfisa.MaterialDemo.exe`
## Зачем отдельно
Основной клиент (`ElectronicPharmacy`) уже построен на `Elfisa.UI` (owner-draw, PharmaTrust, кастомные контролы). Подмешивать MaterialSkin в ту же форму — конфликт двух систем тем и двойная перерисовка.
Это демо показывает, как выглядел бы тот же сценарий (прайс / заказы / загрузка) на готовой библиотеке Google Material.

View File

@ -110,10 +110,10 @@ namespace Elfisa.UI.Controls
FlatStyle = FlatStyle.Flat;
FlatAppearance.BorderSize = 0;
Cursor = Cursors.Hand;
Font = new Font("Segoe UI Semibold", 12f);
Font = new Font("Segoe UI Semibold", 10f, FontStyle.Bold);
Height = ModernButtonStyles.DefaultHeight;
MinimumSize = new Size(64, ModernButtonStyles.DefaultHeight);
Padding = new Padding(14, 6, 14, 6);
MinimumSize = new Size(72, ModernButtonStyles.DefaultHeight);
Padding = new Padding(16, 6, 16, 6);
TabStop = true;
_animTimer = new AnimationTimer(v => { _hoverAnim = v; Invalidate(); });
}
@ -127,11 +127,13 @@ namespace Elfisa.UI.Controls
}
else if (_compactIconMode)
{
Font = new Font("Segoe UI", 16f, FontStyle.Bold);
Font = new Font("Segoe UI Semibold", 13f, FontStyle.Bold);
}
else
{
Font = theme.FontSemibold14;
Font = theme.FontSemibold14 != null
? new Font(theme.FontSemibold14.FontFamily, 10f, FontStyle.Bold)
: new Font("Segoe UI Semibold", 10f, FontStyle.Bold);
}
Invalidate();
}
@ -208,34 +210,61 @@ namespace Elfisa.UI.Controls
g.CompositingQuality = CompositingQuality.HighQuality;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
var bounds = new Rectangle(0, 0, Width - 1, Height - 1);
var radius = theme.CornerRadiusSmall;
var baseColor = ResolveBaseColor(theme);
var fill = baseColor;
var bounds = new Rectangle(0, 0, Width, Height);
var usesGradient = SkillGradientButtonPaint.UsesGradient(_buttonStyle, _compactIconMode);
if (_headerNavMode)
{
var headerBounds = new Rectangle(0, 0, Width - 1, Height - 1);
var headerRadius = theme.CornerRadiusSmall;
var activeBoost = _isActiveItem ? 0.22f : 0.06f;
var hoverBoost = _hoverAnim * 0.14f;
var alpha = (int)(255 * (activeBoost + hoverBoost));
fill = Color.FromArgb(alpha, 255, 255, 255);
using (var brush = new SolidBrush(fill))
var headerFill = Color.FromArgb(alpha, 255, 255, 255);
using (var brush = new SolidBrush(headerFill))
{
GraphicsExtensions.FillRoundedRectangle(g, brush, bounds, radius);
GraphicsExtensions.FillRoundedRectangle(g, brush, headerBounds, headerRadius);
}
if (_isActiveItem)
{
using (var pen = new Pen(Color.FromArgb(180, 255, 255, 255), 1f))
{
GraphicsExtensions.DrawRoundedRectangle(g, pen, bounds, radius);
GraphicsExtensions.DrawRoundedRectangle(g, pen, headerBounds, headerRadius);
}
}
DrawHeaderNavContent(g, bounds, theme);
DrawHeaderNavContent(g, headerBounds, theme);
return;
}
if (usesGradient && !_topNavMode && !_sidebarMode)
{
SkillGradientButtonPaint.Paint(
g,
bounds,
_buttonStyle,
_compactIconMode,
_hover && Enabled,
_pressed && Enabled,
Enabled);
var contentRect = SkillGradientButtonPaint.GetContentRect(
bounds,
_buttonStyle,
_compactIconMode,
Enabled);
var gradientTextColor = Enabled ? theme.TextOnAccent : theme.TextMuted;
DrawActionButtonContent(g, contentRect, gradientTextColor);
return;
}
var flatBounds = new Rectangle(0, 0, Width - 1, Height - 1);
var radius = _compactIconMode ? 6 : 6;
var baseColor = ResolveBaseColor(theme);
var fill = baseColor;
if (_topNavMode)
{
fill = Color.Transparent;
@ -277,17 +306,14 @@ namespace Elfisa.UI.Controls
fill = GraphicsExtensions.Blend(
ResolveHoverColor(theme, baseColor),
baseColor,
1f - (_hoverAnim * 0.35f));
1f - (_hoverAnim * 0.45f));
}
if (!_topNavMode)
if (!_topNavMode && fill.A > 0)
{
if (fill.A > 0)
using (var brush = new SolidBrush(fill))
{
using (var brush = new SolidBrush(fill))
{
GraphicsExtensions.FillRoundedRectangle(g, brush, bounds, radius);
}
GraphicsExtensions.FillRoundedRectangle(g, brush, flatBounds, radius);
}
}
@ -310,7 +336,7 @@ namespace Elfisa.UI.Controls
}
else if ((_buttonStyle == ModernButtonStyle.Secondary
|| _buttonStyle == ModernButtonStyle.DangerSoft
|| _buttonStyle == ModernButtonStyle.Subtle) && !_sidebarMode)
|| _buttonStyle == ModernButtonStyle.Subtle) && !_sidebarMode && !_compactIconMode)
{
if (_buttonStyle == ModernButtonStyle.Subtle && !_hover && !_pressed && !_focused)
{
@ -330,14 +356,14 @@ namespace Elfisa.UI.Controls
using (var pen = new Pen(borderColor, 1f))
{
GraphicsExtensions.DrawRoundedRectangle(g, pen, bounds, radius);
GraphicsExtensions.DrawRoundedRectangle(g, pen, flatBounds, radius);
}
}
}
if (_focused && Enabled && ShowFocusCues && !_headerNavMode && !_topNavMode && !_sidebarMode)
{
var focusBounds = Rectangle.Inflate(bounds, -2, -2);
var focusBounds = Rectangle.Inflate(flatBounds, -2, -2);
using (var pen = new Pen(theme.InputBorderFocus, 2f))
{
GraphicsExtensions.DrawRoundedRectangle(g, pen, focusBounds, Math.Max(2, radius - 1));
@ -366,7 +392,7 @@ namespace Elfisa.UI.Controls
textColor = GraphicsExtensions.Blend(textColor, theme.TextMuted, 0.55f);
}
DrawActionButtonContent(g, bounds, textColor);
DrawActionButtonContent(g, flatBounds, textColor);
}
private void DrawActionButtonContent(Graphics g, Rectangle bounds, Color textColor)
@ -507,9 +533,9 @@ namespace Elfisa.UI.Controls
if (baseColor == theme.AccentPrimary) return theme.AccentPrimaryHover;
if (baseColor == theme.Surface) return theme.GridRowHover;
if (baseColor == theme.DangerSoft) return GraphicsExtensions.Blend(theme.DangerSoft, theme.Danger, 0.08f);
if (baseColor == theme.Danger) return GraphicsExtensions.Blend(theme.Danger, Color.White, 0.08f);
if (baseColor == theme.Success) return GraphicsExtensions.Blend(theme.Success, Color.White, 0.12f);
if (baseColor == theme.Warning) return GraphicsExtensions.Blend(theme.Warning, Color.White, 0.12f);
if (baseColor == theme.Danger) return GraphicsExtensions.Blend(theme.Danger, Color.White, 0.12f);
return GraphicsExtensions.Blend(baseColor, Color.White, 0.08f);
}

View File

@ -6,11 +6,13 @@ using Elfisa.UI.Theming;
namespace Elfisa.UI.Controls
{
/// <summary>
/// WinUI 3style presets for <see cref="ModernButton"/> in WinForms.
/// Стили кнопок по макету Pharma Trust (variant-e-pharma-trust.html).
/// </summary>
public static class ModernButtonStyles
{
public const int DefaultHeight = 32;
public const int DefaultHeight = 34;
public const int ClearSize = 32;
private static readonly Font ActionFont = new Font("Segoe UI Semibold", 10f, FontStyle.Bold);
public static void ApplyAuto(ModernButton button)
{
@ -28,20 +30,20 @@ namespace Elfisa.UI.Controls
if (IsDangerAction(text))
{
ApplyAction(button, ModernButtonStyle.Danger, ResolveIcon(text, "\uE74D"));
ApplyAction(button, ModernButtonStyle.Danger);
return;
}
if (IsPrimaryAction(text))
{
ApplyAction(button, ModernButtonStyle.Primary, ResolveIcon(text, "\uE725"));
ApplyAction(button, ModernButtonStyle.Primary);
return;
}
ApplyAction(button, ModernButtonStyle.Secondary, ResolveIcon(text, null));
ApplyAction(button, ModernButtonStyle.Secondary);
}
public static void ApplyAction(ModernButton button, ModernButtonStyle style, string iconGlyph = null)
public static void ApplyAction(ModernButton button, ModernButtonStyle style)
{
if (button == null)
{
@ -54,15 +56,16 @@ namespace Elfisa.UI.Controls
button.BackgroundImage = null;
button.UseVisualStyleBackColor = false;
button.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button.MinimumSize = new Size(64, DefaultHeight);
if (button.Height < DefaultHeight)
{
button.Height = DefaultHeight;
}
button.IconGlyph = null;
button.AllowTextEllipsis = false;
button.Font = ActionFont;
button.Padding = new Padding(16, 6, 16, 6);
var height = style == ModernButtonStyle.Primary || style == ModernButtonStyle.Danger
? DefaultHeight + 3
: DefaultHeight;
button.Height = Math.Max(button.Height, height);
button.Padding = new Padding(14, 6, 14, 6);
button.Font = (ThemeManager.Colors?.FontSemibold14) ?? new Font("Segoe UI Semibold", 12f);
button.IconGlyph = iconGlyph;
FitToText(button, height);
}
public static void ApplyClear(ModernButton button)
@ -72,24 +75,58 @@ namespace Elfisa.UI.Controls
return;
}
button.ButtonStyle = ModernButtonStyle.DangerSoft;
button.ButtonStyle = ModernButtonStyle.Danger;
button.CompactIconMode = true;
button.Image = null;
button.BackgroundImage = null;
button.UseVisualStyleBackColor = false;
button.AutoSizeMode = AutoSizeMode.GrowAndShrink;
button.Text = "×";
button.Font = new Font("Segoe UI", 13f, FontStyle.Bold);
button.MinimumSize = new Size(32, DefaultHeight);
button.Size = new Size(Math.Max(button.Width, 32), DefaultHeight);
button.Font = new Font("Segoe UI Semibold", 13f, FontStyle.Bold);
button.IconGlyph = null;
button.AllowTextEllipsis = false;
button.MinimumSize = new Size(ClearSize, ClearSize + 3);
button.Size = new Size(ClearSize, ClearSize + 3);
}
public static void FitToText(ModernButton button, int height = DefaultHeight)
{
if (button == null || button.CompactIconMode)
{
return;
}
var text = button.Text ?? string.Empty;
if (text.Length == 0)
{
return;
}
var textWidth = TextRenderer.MeasureText(
text,
button.Font ?? ActionFont,
Size.Empty,
TextFormatFlags.SingleLine).Width;
var width = textWidth + button.Padding.Horizontal + 8;
width = Math.Max(width, 72);
button.MinimumSize = new Size(width, height);
if (button.Width < width)
{
button.Width = width;
}
if (button.Height < height)
{
button.Height = height;
}
}
private static bool IsClearButton(ModernButton button, string text)
{
return text == "×"
|| text.Equals("X", StringComparison.OrdinalIgnoreCase)
|| (button.Width <= 40 && button.Height <= 36 && text.Length <= 2);
|| (button.Width <= 40 && button.Height <= 40 && text.Length <= 2);
}
private static bool IsDangerAction(string text)
@ -116,61 +153,5 @@ namespace Elfisa.UI.Controls
|| lower.Contains("save")
|| lower == "ок";
}
private static string ResolveIcon(string text, string fallback)
{
var lower = text.ToLowerInvariant();
if (lower.Contains("отправ") || lower.Contains("send"))
{
return "\uE725";
}
if (lower.Contains("удал") || lower.Contains("delete"))
{
return "\uE74D";
}
if (lower.Contains("коммент"))
{
return "\uE8BD";
}
if (lower.Contains("сохран") || lower.Contains("save"))
{
return "\uE74E";
}
if (lower.Contains("отмен") || lower.Contains("cancel") || lower.Contains("закры"))
{
return "\uE711";
}
if (lower.Contains("очист") || lower.Contains("clear"))
{
return "\uE894";
}
if (lower.Contains("печат") || lower.Contains("print"))
{
return "\uE749";
}
if (lower.Contains("импорт") || lower.Contains("import"))
{
return "\uE896";
}
if (lower.Contains("подроб") || lower.Contains("info"))
{
return "\uE946";
}
if (lower.Contains("зарег") || lower.Contains("подтвер") || lower == "ок")
{
return "\uE73E";
}
return fallback;
}
}
}

View File

@ -0,0 +1,159 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using Elfisa.UI.Controls;
namespace Elfisa.UI.Helpers
{
/// <summary>
/// Градиент и тень из skill winforms-modern-buttons для ModernButton.
/// </summary>
internal static class SkillGradientButtonPaint
{
private static readonly Color ShadowBase = Color.FromArgb(60, 0, 0, 0);
public static bool UsesGradient(ModernButtonStyle style, bool compactIconMode)
{
return style == ModernButtonStyle.Primary
|| style == ModernButtonStyle.Danger
|| (compactIconMode && style == ModernButtonStyle.Danger);
}
public static int GetCornerRadius(ModernButtonStyle style, bool compactIconMode)
{
if (compactIconMode)
{
return 6;
}
return style == ModernButtonStyle.Primary ? 12 : 8;
}
public static int GetShadowDepth(ModernButtonStyle style, bool compactIconMode, bool enabled)
{
if (!enabled)
{
return 0;
}
if (compactIconMode)
{
return 3;
}
return style == ModernButtonStyle.Primary ? 5 : 4;
}
public static void GetGradient(
ModernButtonStyle style,
bool hover,
bool pressed,
out Color top,
out Color bottom)
{
if (style == ModernButtonStyle.Danger)
{
if (pressed)
{
top = Color.FromArgb(175, 30, 30);
bottom = Color.FromArgb(140, 20, 20);
}
else if (hover)
{
top = Color.FromArgb(245, 110, 110);
bottom = Color.FromArgb(215, 55, 55);
}
else
{
top = Color.FromArgb(235, 87, 87);
bottom = Color.FromArgb(200, 40, 40);
}
return;
}
// Primary preset from skill
if (pressed)
{
top = Color.FromArgb(25, 70, 160);
bottom = Color.FromArgb(15, 55, 130);
}
else if (hover)
{
top = Color.FromArgb(90, 150, 255);
bottom = Color.FromArgb(45, 105, 220);
}
else
{
top = Color.FromArgb(66, 133, 244);
bottom = Color.FromArgb(30, 87, 194);
}
}
public static void Paint(
Graphics g,
Rectangle clientRect,
ModernButtonStyle style,
bool compactIconMode,
bool hover,
bool pressed,
bool enabled)
{
var shadowDepth = GetShadowDepth(style, compactIconMode, enabled);
var radius = GetCornerRadius(style, compactIconMode);
var buttonRect = new Rectangle(
clientRect.X,
clientRect.Y,
clientRect.Width - 1,
clientRect.Height - 1 - shadowDepth);
if (!enabled)
{
using (var brush = new SolidBrush(Color.FromArgb(220, 230, 235)))
{
GraphicsExtensions.FillRoundedRectangle(g, brush, buttonRect, radius);
}
return;
}
for (var i = shadowDepth; i > 0; i--)
{
var alpha = ShadowBase.A * i / (shadowDepth * 3);
using (var shadowBrush = new SolidBrush(Color.FromArgb(alpha, ShadowBase)))
using (var shadowPath = GraphicsExtensions.CreateRoundedRectangle(
new Rectangle(
clientRect.X + i,
buttonRect.Y + i + 1,
buttonRect.Width - i,
buttonRect.Height - 1),
radius))
{
g.FillPath(shadowBrush, shadowPath);
}
}
GetGradient(style, hover, pressed, out var top, out var bottom);
using (var path = GraphicsExtensions.CreateRoundedRectangle(buttonRect, radius))
using (var brush = new LinearGradientBrush(buttonRect, top, bottom, LinearGradientMode.Vertical))
{
g.FillPath(brush, path);
using (var highlightPen = new Pen(Color.FromArgb(40, Color.White), 1f))
{
g.DrawArc(highlightPen, buttonRect.X, buttonRect.Y, radius * 2, radius * 2, 180, 90);
}
}
}
public static Rectangle GetContentRect(Rectangle clientRect, ModernButtonStyle style, bool compactIconMode, bool enabled)
{
var shadowDepth = GetShadowDepth(style, compactIconMode, enabled);
return new Rectangle(
clientRect.X,
clientRect.Y,
clientRect.Width - 1,
clientRect.Height - 1 - shadowDepth);
}
}
}