Favicon

Alert control (DevExpress.WinForms)

Peponi2/6/20254m

C#
SystemWindowsFormsXtraBarsAlerterAlertControl

1. Introduction

screenshot

위의 그림과 같이, DevExpress에는 Alert Windows라는 팝업 윈도우가 있다.

여기서는 HTML/CSS 템플릿 적용 예제를 다뤄본다. 기본 컨트롤도 깔끔하게 구현되어 있지만, 여러가지 데이터를 넣기에는 용이하지 않아 직접 제작하였다.

TIP

비슷한 기능으로 Toast notification 또한 존재한다. 이는 윈도우 알림메세지 (Win10 기준 작업표시줄 끝에 위치) 까지 연결된다.

2. Example

AlertWindow.html
<div class="popup">
    <div class="title">
        <div class="caption">
            <img src='${TitleImageSource}' />
            <span>${Title}</span>
        </div>
        <div class="buttons">
            <img id="pinButton" src='${TitlePinImageSource}' />
            <img id="closeButton" src='${TitleCloseImageSource}' />
        </div>
    </div>
    <div class="body">
        <p class="caption">${Caption}</p>
        <div class="description">
            <img src='${DescriptionImageSource}' />
            <div class="text">
                <p>${Description1}</p>
                <p>${Description2}</p>
            </div>
        </div>
    </div>
    <div class="footer">
        <div class="urls">
            <a href='${FooterUrl1}'>${FooterUrl1}</a>
            <br />
            <a href='${FooterUrl2}'>${FooterUrl2}</a>
        </div>
        <p class="copyright">${Copyright}</p>
    </div>
</div>
AlertWindow.css
p {
    margin: 0;
}
 
.popup {
    width: 350px;
    height: auto;
    padding: 10px 18px 10px 18px;
    border-radius: 10px;
    box-shadow: 0px 8px 16px rgba(0,0,0,0.25);
    border: 1px solid @WindowText/0.2;
    display: flex;
    flex-direction: column;
    font-family: "Segoe UI";
    justify-content: space-around;
    background-color: @Control;
    color: @ControlText;
}
 
.title {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    font-family: 'Segoe UI Semibold';
    font-size: large;
}
 
    .title > .caption {
        padding: 10px 10px 0px 10px;
        display: flex;
        flex-direction: row;
    }
 
        .title > .caption > img {
            width: 18px;
            height: 18px;
        }
 
        .title > .caption > span {
            padding-left: 10px;
        }
 
    .title > .buttons {
        display: flex;
        flex-direction: row;
        padding: 5px 2px 0px 0px;
    }
 
        .title > .buttons > img {
            width: 18px;
            height: 18px;
            padding: 5px;
            border-radius: 2px;
            align-self: center;
        }
 
            .title > .buttons > img:hover {
                border: solid 1px;
            }
 
.body {
    padding: 10px;
}
 
    .body > p {
        margin: 0px 0px 10px 60px;
        font-family: 'Segoe UI Semibold';
        font-size: large;
    }
 
    .body > .description {
        display: flex;
        flex-direction: row;
        padding-bottom: 10px;
    }
 
        .body > .description > img {
            width: 50px;
            height: 50px;
        }
 
        .body > .description > .text {
            padding-left: 10px;
            padding-bottom: 10px;
            font-family: 'Segoe UI';
            font-size: medium;
        }
 
.footer {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    padding: 10px;
    font-family: 'Segoe UI';
    font-size: small;
}
 
    .footer > .copyright {
        text-align: end;
        align-self: flex-end;
    }
AlertData.cs
public class AlertData
{
    public string Title { get; set; } = "";
    public required SvgImage TitleImageSource { get; set; }
    public required SvgImage TitlePinImageSource { get; set; }
    public required SvgImage TitleCloseImageSource { get; set; }
    public string Caption { get; set; } = "";
    public required SvgImage DescriptionImageSource { get; set; }
    public string Description1 { get; set; } = "";
    public string Description2 { get; set; } = "";
    public string FooterUrl1 { get; set; } = "";
    public string FooterUrl2 { get; set; } = "";
    public string Copyright { get; set; } = "";
}
Form1.cs
public partial class Form1 : Form
{
    private readonly DevExpress.XtraBars.Alerter.AlertControl _alert;
    private readonly SvgImageCollection _images;
 
    public Form1()
    {
        InitializeComponent();
        _images = InitImageCollection();
        _alert = InitAlert();
 
        _alert.Show(this);
    }
 
    private SvgImageCollection InitImageCollection()
    {
        var basePath = $@"{Application.StartupPath}\Assets\";
        return new SvgImageCollection(components)
        {
            { "ShortDate", $@"{basePath}ShortDate.svg" },
            { "UnpinButton", $@"{basePath}UnpinButton.svg" },
            { "PinButton", $@"{basePath}PinButton.svg" },
            { "Delete", $@"{basePath}Delete.svg" }
        };
    }
 
    private AlertData GetAlertData()
    {
        return new AlertData()
        {
            Title = "Alert Control",
            TitleImageSource = _images["ShortDate"],
            TitlePinImageSource = _images["UnpinButton"],
            TitleCloseImageSource = _images["Delete"],
            Caption = "Caption",
            DescriptionImageSource = _images["ShortDate"],
            Description1 = "Description1",
            Description2 = "Description2",
            FooterUrl1 = "https://github.com/peponi-paradise/",
            FooterUrl2 = "https://peponi-paradise.vercel.app/",
            Copyright = $"©Peponi, {DateTime.Now.Year}",
        };
    }
 
    private DevExpress.XtraBars.Alerter.AlertControl InitAlert()
    {
        var alert = new DevExpress.XtraBars.Alerter.AlertControl(components);
 
        alert.BeforeFormShow += Alert_BeforeFormShow;
        alert.HtmlElementMouseClick += Alert_HtmlElementMouseClick;
 
        // HTML / CSS
        alert.HtmlTemplate.Template += File.ReadAllText($@"{Application.StartupPath}\Assets\AlertWindow.html");
        alert.HtmlTemplate.Styles += File.ReadAllText($@"{Application.StartupPath}\Assets\AlertWindow.css");
 
        return alert;
    }
 
    private void Alert_BeforeFormShow(object sender, DevExpress.XtraBars.Alerter.AlertFormEventArgs e)
    {
        e.HtmlPopup.DataContext = GetAlertData();
    }
 
    private void Alert_HtmlElementMouseClick(object sender, DevExpress.XtraBars.Alerter.AlertHtmlElementMouseEventArgs e)
    {
        if (e.ElementId == "pinButton")
        {
            e.HtmlPopup.Pinned = !e.HtmlPopup.Pinned;
 
            var data = e.HtmlPopup.DataContext as AlertData;
 
            if (e.HtmlPopup.Pinned)
                data!.TitlePinImageSource = _images["PinButton"];
            else
                data!.TitlePinImageSource = _images["UnpinButton"];
        }
        else if (e.ElementId == "closeButton")
            e.HtmlPopup.Close();
    }
}

3. 참조 자료