[C#] WPF에서 WinForm의 Chart 사용하기


WPF에는 차트가 없어?

내가 못 찾는건지 아니면 진짜 없는건지는 모르겠는데, WPF 프로그램을 개발하던 중 Chart를 넣어야 하는데 없어서 상당히 곤란했다. WinForm의 Chart는 학교에서 데이터베이스 연동 실습을 진행할때 많이 사용했었고, 그 기능이 상당히 많아 후에 WinFrom 프로그램을 만들때도 잘 사용했던 기억이 있는데, 없었다. 일단 WPF로 짜고 있었던 프로그램이라 어떻게든 이걸 해결해야 하니 혹시 무료 컴포넌트가 없나 찾아봐도 무료는 없고 유료는 엄청나게 기능이 좋았다. 물론 개발하는데 유료는 내가 쓰는데도 어려움이 있으니 어떻게 할까 고민하면서 인터넷을 뒤지다보니 WPF안에 WinForm 컴포넌트를 넣는 방법이 있었다. WinForm의 Chart는 내가 많이 써봤고, 이렇게 해결될 수 있다면 굳이 어렵고 돈 들이는 방법을 쓸 필요는 없다. 단 내 코딩 실력이 형편없으니 코드가 어지러워지는건 어쩔 수 없겠지만...


WPF안에 WinFrom을 넣는 방법

https://msdn.microsoft.com/ko-kr/library/ms742875(v=vs.110).aspx

MSDN문서이고 설명도 아주 잘 되어 있다. 이를 사용하면 간단한 WinForm 컨트롤은 쉽게 WPF안에서 보여질 수 있다. 그렇게 나는 Chart를 넣을 수 있겠거니, 의외로 쉽게 풀리겠군. 싶어서 만들었는데, 계속 알 수 없는 문제가 발생했다. 지금은 기억 안나는데, 위의 MSDN문서처럼 적당히 처리하고, 안에 <wf.MaskedTextBox... 라고 되어 있는 부분을 <wf.Chart... 로 넣었으나 컴파일러가 사용할 수 없다는 늬양스의 메시지를 던진다. 알아보니 Chart 컨트롤은 System.Windows.Forms에 속해있지 않고, System.Windows.Forms.DataVisualization.Charting안에 속해있기 때문에 Chart 컨트롤을 사용할 수 없는것. 그래서 이를 응용해서 네임스페이스 매핑을 조금 수정해보았다. xmlns:wf="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms" 대신에 xmlns:wf="clr-namespace:System.Windows.Forms.DataVisualization.Charting;assembly=System.Windows.Forms.DataVisualization.Charting" 이렇게, 그렇게 하면 네임스페이스와 어셈블리를 찾을 수 없다는 에러를 띄운다. 아무래도 이렇게 대응이 가능한 경우는 WinFrom 자체가 포함하고 있는 컨트롤 정도인가 보다.


WPF안에 WinForm을 넣는 다른 방법

즉 위와 같은 방법은 통하지 않는다. 하지만 포기할 이유는 없는게, WinForm이 WPF안에 들어가는건 확인했으니, 이를 응용하거나 다른 방법으로 충분히 넣을 수 있다고 생각했다. using이 어떻든간에 Chart 컨트롤은 WinForm 컨트롤이니까. 우선 WindowsFormsHost가 어떻게 돌아가는지 확인이 필요했다. 여기저기 문서를 확인해보니, WindowsFromsHost는 일종의 컨테이너고, WPF의 일반적인 컨테이너와는 다르게 WinForm을 담아두고 보여줄 수 있는 컨테이너라는것을 확인했다. 그래서 살펴보니 이렇게 사용할 수 있었다.


  • WPF XAML

    WindowsFormsHost Name="windowsFormsHost" />
    
  • WPF CS

    windowsFormsHost.Child = ...
    


그러니까 코드로 WinForm 컨트롤을 완성해두고 WindowsFormsHost에 넣으면 보여진다는 것. 그래서 한번 시도해보았더니, 잘 되었다. 아래 예제는 내가 작업했던 프로그램인데, WPF 프로그램안에 WinFormsHost를 넣고, 그 안에 차트를 넣어 표시한 것이다. 잘 꾸민다면 WPF와 WinForm컨트롤 사이의 위화감 없이 꽤 이쁘게 잘 나온다. (물론 꾸미기는 오지게 어렵다. 이유는 아래부터...)



WPF안에 WinForm Chart를 넣는 방법

그러면 이제 본격적으로 WinForm Chart를 WPF안에 넣어보자. 일단 생각해둬야 할 것이 WPF의 디자이너는 오로지 해당 컨트롤, 그러니까 WinForm Chart가 들어갈 자리만 만드는거고, 당연히 WinForm 디자이너를 쓸 수 없으니 코드로 Chart를 직접 만들고 WPF의 WindowsFormsHost에 붙혀야 한다. WindowsFormsHost는 기존 WPF 컨트롤처럼 Gird에 붙이고, 백그라운드 색상등의 설정이 가능하다. 단, 백그라운드 투명하고, Chart를 투명으로 해서 WPF Window나 Page의 백그라운드가 보이게는 안된다. 아마 Chart쪽에서 보는 백그라운드는 WindowsFormsHost가 만들아준 공간 안이기 때문에 그 안 정보가 없어 그런 듯. 자세한건 모른다. 디자인적으로 뭔가 하려고 하면 안되는게 간혹 나오는데 생각해보면 뭔가 이해가 된다.


사족은 그만하고 WPF쪽 코드는 다음과 같이 처리하면 된다.

    <WindowsFormsHost Name="chart">
    </WindowsFormsHost>


만약 어느 Gird에 넣고 싶고, 백그라운드를 설정해주고 싶거나 기타 다른 옵션을 주고 싶다면 기존 다른 WPF 컨트롤을 디자인 하는 것처럼 하면 된다. 디자이너 쓰면 편하다. 물론 이름도 원하는대로 설정해서 한 Window나 Page에 여러개의 WindowsFormsHost를 넣을 수 도 있다. 아래는 WindowsFormsHost를 Gird에 붙이고, 백그라운드를 설정해 준 WPF 코드이다.

    <WindowsFormsHost Name="chart" Grid.Row="1" Grid.Column="1" Foreground="{x:Null}"  Background="#FF2B2B2B">
    </WindowsFormsHost>


이렇게 간단하게 두줄이면 WPF쪽은 준비가 끝났다. 이제 남은건 이 chart WindowsFormsHost에 담을 Chart를 준비하는건데, 앞에서도 말햇지만 코드로 직접 짜줘야 한다. 예를 들어 위에서 본 예제처럼 만들려면, 우선 System.Windows.Forms.DataVisualization.Charting;를 선언해서 Chart 컨트롤을 사용할 수 있게 하고, Chart 객체를 하나 선언하고, Chart 컨트롤에 필요한 부분(ChartArea, Series, Legend등)을 구성해 준 다음, WindowsFormsHost에 넣으면 된다. 데이터까지 담으면 그래프가 완벽히 그려진다. 위에서 본 예제를 위해 만든 코드는 아래와 같다. 물론 이는 예제일 뿐이고, 실제 만든 프로그램에서 사용된 코드이므로 바로 사용할 순 없다. 참고만 하자. 직접 쉽게 만들고 사용하는 방법은 아래에 추가로 설명한다.

     private void graphInit(List<SubMcu> subMcuList)
    {
        Chart chartView = new Chart();
        Title title = new System.Windows.Forms.DataVisualization.Charting.Title();
        title.Text = "안전도";
        title.ForeColor = System.Drawing.Color.White;
        chartView.Titles.Add(title);
        chartView.BackColor = System.Drawing.Color.FromArgb(0, 0, 0, 0);

        ChartArea chartArea = new ChartArea();
        chartArea.Name = "Safety";
        chartArea.BackColor = System.Drawing.Color.FromArgb(0, 0, 0, 0);
        chartArea.AxisX.IntervalAutoMode = IntervalAutoMode.FixedCount;
        chartArea.AxisX.Interval = 1;
        chartArea.AxisX.TitleForeColor = System.Drawing.Color.LightGray;
        chartArea.AxisX.LineColor = System.Drawing.Color.LightGray;
        chartArea.AxisX.MajorGrid.LineColor = System.Drawing.Color.LightGray;
        chartArea.AxisX.MajorGrid.Enabled = false;
        chartArea.AxisX.LabelStyle.ForeColor = System.Drawing.Color.LightGray;
        chartArea.AxisX.MajorTickMark.LineColor = System.Drawing.Color.LightGray;
        chartArea.AxisY.LineColor = System.Drawing.Color.LightGray;
        chartArea.AxisY.TitleForeColor = System.Drawing.Color.LightGray;
        chartArea.AxisY.Maximum = 100;
        chartArea.AxisY.TitleForeColor = System.Drawing.Color.LightGray;
        chartArea.AxisY.MajorGrid.LineColor = System.Drawing.Color.LightGray;
        chartArea.AxisY.LabelStyle.ForeColor = System.Drawing.Color.LightGray;
        chartArea.AxisY.MajorTickMark.LineColor = System.Drawing.Color.LightGray;
        chartView.ChartAreas.Add(chartArea);

        Series series = new Series();
        series.ChartArea = "Safety";
        series.BackGradientStyle = GradientStyle.TopBottom;
        series.ChartType = SeriesChartType.Column;
        series.XValueType = ChartValueType.String;
        series.BackSecondaryColor = System.Drawing.Color.Aquamarine;
        series.LabelForeColor = System.Drawing.Color.White;
        series.Color = System.Drawing.Color.SteelBlue;
        series.IsValueShownAsLabel = true;
        chartView.Series.Add(series);

        Legend legend = new Legend();
        legend.Enabled = false;
        chartView.Legends.Add(legend);

        chart.Child = chartView;

        foreach (SubMcu subMcu in subMcuList)
        {
            chartView.Series[0].Points.AddXY(subMcu.Id, subMcu.SafetyFactor);
        }
    }


처음에는 저걸 직접 일일이 쳐 가면서 만들었다. 디자이너를 직접 사용할 수 도 없을 뿐더러, 후에는 조금 편법으로 학교 수업에서 배웠던 코드로 차트 컨트롤 만드는 예제를 통해 만든 다음 붙혀넣었지만 뭔가 제대로 나오지 않는 부분이 많았기 때문. 물론 하나하나 세세하게 신경써가면서 만드니 결국 잘 나오긴 하더라. 그래서 나는 저 코드를 메모장에 복사해서 그때그때 필요한 부분에 붙혀서 사용했는데, 후에 간단한 방법을 찾게 되었다. 그 방법은 디자이너를 이용하는것. 

WinForm 디자이너를 사용할 수 없다고 앞에 말했는데, 그냥 디자이너를 위한 새로운 프로젝트를 하나 만들어서 그기서 디자인 한 다음, 디자이너가 생성해 둔 코드를 복사해서 넣으니까 꽤 잘 돌아가더라. 물론 잘 안보이거나 안나오는것들은 일일이 수정이 필요하긴 하지만, 각각의 컨트롤을 직접 코드로 설정해주는 것 보다는 훨씬 쉬웠다. 단 디자이너로 만들 경우 주의할 사항이 있는데, 색상 설정등은 필이 직접 해 줄 것.


WinForm 프로그램에서는 컨트롤의 색성 설정을 해주지 않아도 기본 색상이 선택되어 잘 나오지만, WPF에 그대로 사용하니 나오지 않는 경우가 상당히 많다. 그러니 필이 기본값(비워져 있는등)인 것들은 직접 사용할 색상을 선택해주면 디자이너가 생성하는 코드에 그 색상이 지정되고, 결과적으로 WPF에서도 문제없이 나온다. 여튼 이렇게 상세하게 만든 Chart 컨트롤의 코드는 디자이너가 작성한 코드로 들어가 있다.(위 스크린샷에서는 아주 짧지만, 실제로 사용하려고 만들면 대략 50줄은 그냥 넘긴다)

이를 복사해서 사용할 곳에 넣어준 후 마지막에

    chart.Child = chartView;

를 써주면 잘 들어간다.(물론 각 객체의 이름은 잘 맞춰야 한다)

Minny_

,

[C#] WPF 프로그램의 노티바(시스템 트레이) 아이콘 생성


그냥 바로 못한다


현장실습 중 만든 프로그램은 기본적으로 작동중에는 백그라운드에서 돌아야 한다. 라는 조건이 붙는데, 아래 글(2017/02/06 - [Tip/Develop] - [C#] WinForm 프로그램의 노티바(시스템 트레이) 아이콘 생성)과 같은 조건이다. 같은 C#이고, 전에 만들어둔 코드를 그대로 붙혀 두면 될 것이라고 쉽게 생각했었는데, 그게 아니었다. 당장 NotifyIcon 이라는 도구상자 컴포넌트가 없다.


이유를 알아보니, NotifyIcon이라는 컴포넌트는 System.Windows.Forms 안에 포함되어 있는 거고, WPF는 Forms를 Using하지 않으니 바로 사용할 수 없는 것. 간단하게 System.Windows.Forms을 Using하면 되지 않을까 라는 생각을 하지만, 그렇게 할 경우 MessageBox와 같은 System.Windows.Forms와 System.Windows.Controls에 동일하게 존재하는 객체들의 모호성이 생긴다.


결국은 그냥 NotifyIcon 객체를 직접 지시하여 생성해 주고 그 설정을 맞춰주면 된다. 사실 WinForm의 방법과 크게 차이는 없지만, 직접 지시를 해줘야 하는 부분이 있어야 한다는것.


배경이 똑같으니 목적이나 방법 또한 아래 글(2017/02/06 - [Tip/Develop] - [C#] WinForm 프로그램의 노티바(시스템 트레이) 아이콘 생성)과 거히 동일하니 뭘 할 건지는 해당 글을 먼저 보고 오는것을 추천.



그래서 하는 방법


매인 윈도우가 있는 클래스에 전역변수로 NotifyIcon을 직접 생성해 주고, init를 할 때 해당 객체를 적당히 맞춰준다. WinForm에서 생성하는것과는 다른 부분이 바로 이 부분이고, 그 뒤의 부분은 동일. 필요한 상황에 맞는 이벤트나 메서드에 NotifyIcon을 보여주고 창을 숨긴다. NotifyIcon에 더블클릭 이벤트를 걸어주고, 이 이벤트 안에는 NotifyIcon을 숨기고 창을 보이게 하는것.


1. NotifyIcon을 생성하고 설정

 public partial class MainWindow : Window{
    public System.Windows.Forms.NotifyIcon notify;

    public MainWindow(){
        InitializeComponent();
    }
    ...


전역으로 하나 생성해주고,


private void Window_Loaded(object sender, RoutedEventArgs e){
     notify = new System.Windows.Forms.NotifyIcon();
     notify.Icon = Properties.Resources.ico;
     notify.Text = "PLC 통신";
     notify.DoubleClick += Notify_DoubleClick;

     ...
}


Window가 로드될 때 이름과 아이콘, 그리고 이벤트를 지정해 준다. WinForm때와 마찬가지로 아이콘은 지정해주지 않으면 아무리 잘해도 보이지 않으니강조하는 이유는 내가 당해봐서… 꼭 빼먹지 말고 해주자.


2. 창이 숨겨지고 시스템 트레이 아이콘이 보이게

3. 시스템 트레이 아이콘이 숨겨지고 창이 보이게


는 사실 WinForm때와 동일하다. 2번의 경우 창이 닫길때나 숨겨야 할 상황의 메서드에 창을 숨기고 notifyIcon을 보이게 하면 되고, 3번의 경우에는 그 반대. 물론 그렇다고 WinForm의 코드를 그대로 복붙해서는 안된다. 창의 숨김, 보이기가 WinForm과는 다르니까 신경써주자.

 private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        { 
            if(SharedKeyword.isRunning == true)
            {
                notify.Visible = true;
                this.Visibility = Visibility.Collapsed;
                e.Cancel = true;
                return;

            }
private void Notify_DoubleClick(object sender, EventArgs e)
        {
            notify.Visible = false;
            this.Visibility = Visibility.Visible;
        }


Minny_

,

[C#] WinForm 프로그램의 노티바(시스템 트레이) 아이콘 생성



간단한것만…


현장실습 중에 만든 프로그램에서 가장 기본이 되어야 하는것이 작동중에는 백그라운드에서 돌아야 한다.


막 메뉴가 추가되거나, 아이콘이 상태에 따라서 바뀌거나 하는것이 아닌 간단한 정도만 구현하면 됬었다. 그러니까 순전히 프로그램이 작동중일때(미들웨어로써 역활을 하고 있을때) 프로그램 종료를 하면 종료가 되는것이 아닌

  1. 창이 숨겨지고 2. 시스템 트레이(노티바)에 띄워지도록하고 3. 노티바 아이콘을 더블클릭하면 다시 창이 보이며 시스템 트레이(노티바) 아이콘은 없어지도록

하는것.



위의 목적대로 만드는 방법


WinForm 프로그램은 만들기 쉽다. 도구 상자에서 NotifyIcon 을 가져와서, 아이콘과 이름을 지정한 후, 필요한 상황에 맞는 이벤트나 메서드에 NotifyIcon을 보이게 하고 창을 숨기면 된다. NotifyIcon의 더블클릭 이벤트를 생성해주고 이 이벤트 안에는 NotifyIcon을 숨기고 창을 보이게 하면 끝.



1.NotifyIcon을 생성한다


그냥 도구상자에서 찾아 끌어오면 된다. 그리고 해당 콤포넌트의 아이콘과 이름을 지정해준다. 귀찮으니 이름은 기본값으로 놔두었다. 아이콘은 없으면 안된다. 아이콘이 없으면 아무리 잘 해놔도 보이지 않는다. 아무 아이콘이나 대충 끌어와 리소스에 넣고 맞춰주자.


2.창이 숨겨지고 시스템 트레이 아이콘이 보이게

위와 같은 목적으로 하려면 프로그램이 종료되지 않게 해야 하므로, 해당 프로그램의 매인 From에 FormCosing 이벤트를 붙이고, 해당 메서드에 다음과 같이 작성하면 된다.


private void MoisturmMeasurmentProgram_FormClosing(object sender, FormClosingEventArgs e){
        if (!btnStart.Enabled)
        {
            e.Cancel = true;
            notifyIcon1.Visible = true;
            this.Visible = false;
            return;
        }
    }


물론 상황에 따라 저 if안에 검사할 조건을 바꿔야 한다. 나 같은 경우는 btnStart라는 버튼을 활성화, 비활성화 하는것에 따라 프로그램의 작동 상황을 구분하니 이렇게 한 것이고… 물론 이렇게 하면 안된다!

FromClosing 이벤트를 취소하기 위해 e.Cancel = true를 하였고, 시스템 트레이(노티바) 아이콘을 보이게 한 후, 이 From을 보이지 않게 한다.


3.시스템 트레이 아이콘이 숨겨지고 창이 보이게

창을 다시 띄우고 싶을때 해당하는 메서드에 동작을 넣어주면 된다. 소제목도 2번과 반대이고 로직또한 2번과 반대이니 어렵지 않다. 나는 간단하게 시스템 트레이 아이콘을 더블클릭하면 해당 동작이 되게 한다. 그러기 위해서는 시스템 트레이(노티바) 아이콘의 더블클릭 이벤트를 붙혀주면 된다.


이벤트 붙이는거야 뭐 속성에서 이벤트를 더블클릭해서 메서드 생성을 해주던지, 직접 코드에서 붙혀주던지 알아서 하면 된다. 아래는 해당 이벤트 코드이다.

private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e){
        this.Visible = true;
        this.Activate();
    }



Minny_

,