훈스닷넷에 PictureBox위에 1cm 간격으로 눈금을 그리고 싶어하는 질문이 있어서

 

데모 프로젝트를 작성해보았습니다.

 

 

 

코드는 아래와 같습니다.

 

 

public partial class Form1 : Form

{

    /// <summary>

    /// 1pixel에 해당하는 센치

    /// </summary>

    const float OnePixelCentimetre = 37.79f;

    /// <summary>

    /// 수직라인 길이

    /// </summary>

    const int VerticalLineHeight = 50;

    /// <summary>

    /// 그리드 라인 색상

    /// </summary>

    readonly Pen gridLineColor;

 

    public Form1()

    {

        InitializeComponent();

        this.pictureBox1.Image = Image.FromFile("./Chrysanthemum.jpg");

 

        this.pictureBox1.Paint += PictureBox1_PaintEventHandler;

        this.SizeChanged += Form1_SizeChangedEventHandler;

        this.gridLineColor = Pens.Yellow;

    }

 

    /// <summary>

    /// 폼의 사이즈 변경 이벤트 처리기

    /// </summary>

    /// <param name="sender"></param>

    /// <param name="e"></param>

    private void Form1_SizeChangedEventHandler(object sender, EventArgs e)

    {

        this.pictureBox1.Invalidate();

    }

 

    /// <summary>

    /// PictureBox Paint 이벤트 처리기

    /// </summary>

    /// <param name="sender"></param>

    /// <param name="e"></param>

    private void PictureBox1_PaintEventHandler(object sender, PaintEventArgs e)

    {

        PictureBox pb = (PictureBox)sender;

        Graphics g = e.Graphics;

 

        //Picturebox의 중앙 위치

        float centerYPosition = pb.Height / 2.0f;

 

        //수평 라인 그리기

        g.DrawLine(gridLineColor, 0f, centerYPosition, pb.Width, centerYPosition);

 

        //현재 Picturbox에서 1cm 개수

        double cmStep = pb.Width / OnePixelCentimetre;

 

        //세로 라인의 절반

        int verticalLineHeightHalf = VerticalLineHeight / 2;

 

        //수직 라인 그리기

        for (float i = 0; i < pb.Width; i += OnePixelCentimetre)

        {

            PointF beginPoint = new PointF(i, centerYPosition - verticalLineHeightHalf);

            PointF endPoint = new PointF(i, centerYPosition + verticalLineHeightHalf);

            g.DrawLine(gridLineColor, beginPoint, endPoint);

        }

    }

}

 

 

 

프로젝트(vs2015 작성) : HowToDrawOnPicturebox.zip

 

 

 

위와 같이 PictureBox의 이미지를 투명하게 만드는 코드이다.

 

1. 불러올 파일의 경로의 Image 개체를 생성 

 

2. ChangeOpacity 메서드를 통해 이미지의 알파값을 변경하여 새로운 비트맵 개체 생성

 

3. 생성한 비트맵 개체를 PictureBox Image 속성으로 설정

 

4. 생성한 Image 개체 제거

 

 

public partial class Form1 : Form

{

    public Form1()

    {

        InitializeComponent();

    }

 

    private void btnLoad_Click(object sender, EventArgs e)

    {

        OpenFileDialog of = new OpenFileDialog();

 

        if (of.ShowDialog() == DialogResult.OK)

        {

            try

            {

                this.SetImage(of.FileName);

            }

            catch (Exception ex)

            {

                MessageBox.Show(ex.Message);

            }

        }

    }

 

    private void SetImage(string fileName)

    {

        try

        {

            Image img = Image.FromFile(fileName);

            this.pictureBox1.Image = ChangeOpacity(img, 0.5f);

            img.Dispose();

        }

        catch (Exception ex)

        {

            throw ex;

        }

    }

 

    /// <summary>

    /// 해당 이미지의 투명도를 변경한다.

    /// </summary>

    /// <param name="img"></param>

    /// <param name="opacityvalue"></param>

    /// <returns></returns>

    public Bitmap ChangeOpacity(Image img, float opacityvalue)

    {

        Bitmap bmp = new Bitmap(img.Width, img.Height);

        Graphics graphics = Graphics.FromImage(bmp);

        ColorMatrix colormatrix = new ColorMatrix();

        colormatrix.Matrix33 = opacityvalue;

        ImageAttributes imgAttribute = new ImageAttributes();

        imgAttribute.SetColorMatrix(colormatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

        graphics.DrawImage(img, new Rectangle(0, 0, bmp.Width, bmp.Height), 0, 0, img.Width, img.Height, GraphicsUnit.Pixel, imgAttribute);

        graphics.Dispose();

        return bmp;

    }

}

 

 

 

vs2015 프로젝트 :  PictureBox투명도.zip

 

출처 : https://raviranjankr.wordpress.com/2011/05/25/change-opacity-of-image-in-c/

 

 

 

 

VisualStudio에서 컨트롤 배치 할때와 같이 도구상자에서 드래그&드롭하여 배치하는 예제이다.

 

개발 내용 :

1. 디자인 모드일때 투명한 패널을 앞에두어서 Button,TextBox와 같이 클릭에 반응하는 컨트롤들이 반응하지 않게두었다.

2. Painted 이벤트를 두어서 자식 컨트롤 최상단에 Graphics 개체를 이용해 Drawing을 할수있게 하였다.

 

 

컨트롤드래그앤드랍추가및크기조절.zip

 

UI쓰레드를 여러개 두어서 UI갱신을 빠르게할 목적으로 테스트하였다.

 

윈도우 프로그램은 하나의 메세지 루프&큐를 가지기 때문에 많은 윈도우들을 갱신하면 부하가 걸린다.

 

그래서 각 윈도우 마다 메세지 루프&큐를 갖게 하여 성능이 향상 됨을 테스트하였다.

 

 

테스트 시나리오

1. 윈도우 20개 생성

2. 윈도우 마다 10ms 타이머 발생하여 랜덤한 값을 그리드 갱신

3. 갱신 시간 확인

 

테스트 결과

1. 한개의 UI 쓰레드 테스트

 

 

 

 

 

2. 20개의 UI 쓰레드 테스트

 

 

 

 

 

 

주요 코드

 

/// <summary>

/// 새로운 UI Thread 가진 View 추가한다.

/// </summary>

private void AddNewUIThreadSubView()

{

    SubView subView = null;

    Thread thread = new Thread(

                    () =>

                    {

                        subView = new SubView();

                        forms.Add(subView);

                        Application.Run(subView);

                    });

    thread.SetApartmentState(ApartmentState.STA);

    thread.Start();

} 

 

 

기타

1. UI 쓰레드를 생성 후에 해당 Form에 접근할때에는 다른 쓰레드이므로 Invoke를 통하여 조작해야한다.

2. MDI는 SetParent 함수를 이용해 해결한다.

3. UI Thread가 여러개일때는 CPU 사용률이 올라갔지만, UI 갱신은 빨라졌다.

 

 

의문점

1. 한개의 UI 쓰레드에서 SetParent 함수를 설정하니 성능이 올라갔다. 이유를 고민해봐야함..ㅠㅠ

2. 1번의 이유로 ..과연 멀티 UI 쓰레드 때문에 성능이 향상된건지 의문이다................ㅡㅜ

 

 

 

 

소스코드

 

HowToMultiUIThreads.zip

DataGridViewComboBoxCell이 클릭 되었을 때, 콤보박스 내용들을 확인하는 코드입니다.

 

 

 

this.grid.CellClick += MultiHeaderGrid_CellClick;

 

 

 

void grid_CellClick(object sender, DataGridViewCellEventArgs e)

{

    if (this.CurrentCell.EditType == typeof(DataGridViewComboBoxEditingControl))

    {

        SendKeys.Send("{F4}");

    }

} 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Office.Tools.Ribbon;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;

namespace ExcelAddIn1
{
    public partial class Ribbon1
    {
        Worksheet _sheet;
        public double pppx = -1, pppy = -1;
        public double fbTop = -1, fbLeft = -1;
        public double hLeft = -1, hTop = -1;
        public bool init = false;

        const int LOGPIXELSX = 88;
        const int LOGPIXELSY = 90;
        [DllImport("user32.dll")]
        static extern IntPtr GetDC(IntPtr hWnd);
        [DllImport("user32.dll")]
        static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
        [DllImport("gdi32.dll")]
        static extern int GetDeviceCaps(IntPtr hdc, int nIndex);

        private void PixelsPerPointX()
        {
            if (!this.init)
            {
                IntPtr hdc = GetDC(new IntPtr(0));
                int PixPerInchX = GetDeviceCaps(hdc, LOGPIXELSX);
                double pppx = PixPerInchX / 72.0; //72 is the points per inch
                ReleaseDC(new IntPtr(0), hdc);
                this.pppx = pppx;
            }
        }

        private void PixelsPerPointY()
        {
            if (!this.init)
            {
                IntPtr hdc = GetDC(new IntPtr(0));
                int PixPerInchY = GetDeviceCaps(hdc, LOGPIXELSY);
                double pppy = PixPerInchY / 72.0; //72 is the points per inch            
                ReleaseDC(new IntPtr(0), hdc);
                this.pppy = pppy;
            }
        }

        private void GetFormulaBarAndHeadingsDim()
        {
            if (!this.init)
            {
                bool formBar = Globals.ThisAddIn.Application.DisplayFormulaBar;
                bool headings = Globals.ThisAddIn.Application.ActiveWindow.DisplayHeadings;
                double beforeH, afterH, beforeW, afterW;

                //check the size of the formula bar                
                beforeH = Globals.ThisAddIn.Application.ActiveWindow.Height;
                beforeW = Globals.ThisAddIn.Application.ActiveWindow.Width;
                Globals.ThisAddIn.Application.DisplayFormulaBar = false;
                afterH = Globals.ThisAddIn.Application.ActiveWindow.Height;
                afterW = Globals.ThisAddIn.Application.ActiveWindow.Width;                
                Globals.ThisAddIn.Application.DisplayFormulaBar = true;                
                this.fbLeft = afterW - beforeW;
                this.fbTop = afterH - beforeH;



                this.hLeft = 0;
                this.hTop = 0;

                Globals.ThisAddIn.Application.DisplayFormulaBar = formBar;
                Globals.ThisAddIn.Application.ActiveWindow.DisplayHeadings = headings;
            }
        }

        private void log(String s)
        {
            this.label3.Label += s + " | ";
        }

        private void Ribbon1_Load(object sender, RibbonUIEventArgs e)
        {            
            this._sheet = Globals.ThisAddIn.Application.ActiveSheet;
            this._sheet.SelectionChange +=new DocEvents_SelectionChangeEventHandler(sheet_SelectionChange);
        }

        private void sheet_SelectionChange(Range rng)
        {
            MessageBox.Show("Select changed!");
        }


        private void CellTopLeftPixels(Range rng)
        {
            if (!init)
            {
                PixelsPerPointX();
                PixelsPerPointY();
                GetFormulaBarAndHeadingsDim();
                init = true;
            }

            double appTop = Globals.ThisAddIn.Application.Top;
            double appLeft = Globals.ThisAddIn.Application.Left;
            long RibbonHeight = Globals.ThisAddIn.Application.CommandBars["Ribbon"].Height;
            

            this.log("aT " + appTop);
            this.log("aL " + appLeft);
            this.log("rH " + RibbonHeight);
            this.log("px " + this.pppx);
            this.log("py " + this.pppy);
            this.log("fY " + this.fbTop);
            this.log("fX " + this.fbLeft);
            this.log("hY " + this.hTop);
            this.log("hX " + this.hLeft);
            this.log("rT " + rng.Top);
            this.log("rL " + rng.Left);
            this.log("1T " + (appTop + RibbonHeight + rng.Top + this.fbTop + this.hTop));
            this.log("1L " + (appLeft + rng.Left + this.fbLeft + this.hTop));


            long top = (long)((appTop + RibbonHeight + rng.Top + this.fbTop + this.hTop) * this.pppy);
            long left = (long)((appLeft + rng.Left + this.fbLeft + this.hLeft) * this.pppx);
            this.label1.Label = "left: " + left + " top: " + top;


            long topc = (long)((appTop + RibbonHeight + rng.Top + this.fbTop + this.hTop + rng.Height) * pppy);
            long leftc = (long)((appLeft + rng.Left + rng.Width + this.fbLeft + this.hTop) * pppx);
            this.label2.Label = "left: " + leftc + " top: " + topc;
        }

        private void button1_Click(object sender, RibbonControlEventArgs e)
        {
            this.label3.Label = "";
            CellTopLeftPixels(Globals.ThisAddIn.Application.ActiveCell);
        }        
    }
}
 

출처 : https://svn.kwarc.info/repos/sissi/trunk/win_office_alex/ExcelAddIn1/ExcelAddIn1/Ribbon1.cs

TabControl에 추가 되어 있는 Tab을 드래그하여 빼내면 윈도우가 나오게 하는 기능을 구현하였습니다.

데모 프로그램은 아래와 같습니다.

 

 

 

 

1. DragDrop 탭추가 : Drag&Drop하여 넣고 뺄 수 있는 탭을 추가합니다.

 

 

 

2. DragDrop MDI탭추가 : Drag&Drop하여 넣고 뺄 수 있는 MDI 윈도우를 추가합니다.

 

 

 

3. 일반 탭추가 : 일반탭을 추가하여 탭밖으로 빠지질않는것을 확인합니다.

 

 

4. 일반 폼을 추가 : 일반폼을 추가하여 탭안에 삽입이 안되는 것을 확인합니다.

 

 

프로젝트 파일 : KDYFramework.Winform.Controls.DragDropTabControl.zip

 

 (VS2010 작성)

.Net에서 제공해주는 DataGridView 컨트롤은 멀티헤더 기능을 제공해주지 않습니다.

 

기능이 필요해서 직접 구현했습니다. 필요하신분은 참조하세요~

 

 

 

 

 

 

- 멀티헤더 기능

 

 

-기타 기능

 

 

 

 

-다중 선택 (Ctrl or Shift 누르고 헤더 클릭시)

 

 

- 일괄적용 (선택 영역 데이터 한번에 변경)

 

 

 

 

프로젝트 파일(VS2013 작성) :  

 

DLL.zip

 

Source.zip

 

 

 

 

 

 

1.   CodeDom Lambda 비교

 

 

CodeDom

Lambda

변수 선언

가능                                      

불가 (단 하나의 표현식만 가능)

함수 작성

사용자가 C# 문법을 알아야함

간단하게 식 사용

Math 및 기타

라이브러리 지원

지원

지원(lambda-parser 오픈소스 사용)

컴파일 오류

오류 위치 확인 가능

오류 위치 확인 어려움

컴파일 형태

소스코드->CodeDom 그래프->IL 컴파일 (개발 복잡)

1.     Dll 파일 형태

2.     메모리 형태

소스코드->Expression tree->IL 컴파일

(소스코드 관리 편함)

1.     메모리 형태

호출 속도

1.     Func Delegate로 컴파일시 빠름, 단 함수 형태가 정의 되어있어야함

2.     DynamicInvoke는 느리지만 유연함

 

 

 

 

 

 

 

1.1 변수 선언

Lambda는 단 하나의 표현식만 가능하다. 모든 클래스나 메소드, 또는 선언문은 위해 설계되지 않아서 변수 사용은 불가능하다.

 

- CodeDom

 

-Lambda

 

 

 

 

1.2 Math 및 기타 라이브러리 사용

-CodeDom

 

 

-Lambda

 

 

 

1.3 컴파일 오류

-CodeDom

 

-Lambda

 

1.4 컴파일

CodeDOM CodeDOM 그래프, Lambda는 익스프레션 트리 형태로 변환되어 컴파일 된다. 익스프레션 트리는 CodeDOM과 크게 다를 바가 없지만, 두가지 차이가 있다.

첫째, 익스프레션 트리는 단 하나의 표현식만 가능하다. 모든 클래스나 메소드, 또는 선언문은 위해 설계되지 않았다.

둘째, C#은 언어에서 람다 식과 마찬가지로 직접적으로 익스프레션 트리를 지원한다.

 

-CodeDom

Windows에서는 개별 어셈블리 언로드를 지원하지 않는다. 대신 프로그램 도메인을 언로드하면 어셈블리도 같이 언로드가 된다. 하나의 프로세스에서는 주 프로그램 도메인(1)과 보조 프로그램 도메인(N)을 만들 수 있다. 보조 프로그램 도메인을 하나 생성하여 그 도메인에 어셈블리를 로드하여 사용하고, 재 컴파일시 도메인을 언로드하고 다시 로드하는 방식으로 수행하면 프로그램을 재시작하지 않고 사용자 정의 함수를 호출 할 수 있다.

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace CalcDynamicCompile {     class AssemblyHelper :IDisposable     {         AppDomain newAppDomain;         AssemblyLoader proxy;                  internal void LoadAssembly(string assemName)         {             AppDomainSetup setup = new AppDomainSetup();             setup.ShadowCopyFiles = "true";             newAppDomain = AppDomain.CreateDomain("newDomain"null, setup);             proxy = newAppDomain.CreateInstanceAndUnwrap(typeof(AssemblyLoader).Assembly.FullName, 

typeof(AssemblyLoader).FullName) as AssemblyLoader;             proxy.LoadAssembly(assemName);         }         internal string LoadCode()         {             return proxy.LoadCode();         }                  internal void CreateInstance()         {             this.proxy.CreateInstance();                      }         internal void CreateDelegate(string methodName)         {             this.proxy.CreateDelegate(methodName);         }

        void IDisposable.Dispose()         {             proxy = null;             AppDomain.Unload(newAppDomain);             newAppDomain = null;         }         internal void Test(object[] param)         {             this.proxy.Test(param);         }     } }

 

 문제점은 도메인간에는 데이터가 공유되지 않기 때문에, MarshalByRefObject(응용 프로그램 도메인 간 경계를 넘어 개체에 엑세스할 수 있는 Abstract Class) Class를 상속 받은 개체에서만 사용자 정의 함수를 호출 할 수 있다. AppDomain으로 메소드 호출시에 Type이나 Assembly 등의 개체가 전달되거나 반환되면 상대방 AppDomain 측에서 해당 어셈블리의 로드가 자동으로 일어난다. 따라서 어셈블리의 동적 로드와 언로드의 목적 달성을 위해서는 이런 종류의 호출은 피해야 한다.

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Reflection; using System.Diagnostics; namespace CalcDynamicCompile {     public class AssemblyLoader : MarshalByRefObject     {         Assembly assem;         object instance;

Delegate dele;         Func<floatfloatfloat> func;         public void LoadAssembly(string filename)         {             assem = Assembly.LoadFile(filename);         }         public override object InitializeLifetimeService()         {             // 이 객체가 원격 도메인 유휴 시간에 의해 자동으로 삭제되지 않게 함             return null;         }         internal string LoadCode()         {             string code = string.Empty;             string[] list = assem.GetManifestResourceNames();             if (list.Length > 0)             {                 using (System.Resources.ResourceReader sr = new System.Resources.ResourceReader(assem.GetManifestResourceStream(list[0])))                 {                     string resourceType;                     byte[] resourceData;                     sr.GetResourceData("code.txt"out resourceType, out resourceData);                     code = Encoding.ASCII.GetString(resourceData).Remove(0, 4);                 }             }             return code;         }         internal void CreateInstance()         {             Type type = assem.GetExportedTypes()[0];             instance = Activator.CreateInstance(type);         }         internal void CreateDelegate(string methodName)         {             Type type = assem.GetExportedTypes()[0];             MethodInfo info = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); ;             List<Type> tArgs = new List<Type>();             foreach (var param in info.GetParameters())                 tArgs.Add(param.ParameterType);             tArgs.Add(info.ReturnType);             Type delDecltype = System.Linq.Expressions.Expression.GetDelegateType(tArgs.ToArray());             this.dele = Delegate.CreateDelegate(delDecltype, this.instance, info);             this.func = (Func<floatfloatfloat>)this.dele;         }         internal void Test(object[] param)         {             Stopwatch sw = Stopwatch.StartNew();             float res = 0;             for (int i = 0; i < 100000; i++)             {                 if (param == null)                 {                     res = (float)this.dele.DynamicInvoke(null);                 }                 else                 {                     res = (float)this.dele.DynamicInvoke(param);                 }             }             sw.Stop();             Console.WriteLine(string.Format("DynamicInvoke : {0}", sw.Elapsed.ToString()), res.ToString());             sw = Stopwatch.StartNew();             for (int i = 0; i < 100000; i++)             {                 res = this.func((float)param[0], (float)param[1]);             }             sw.Stop();             Console.WriteLine(string.Format("FuncInvoke : {0}", sw.Elapsed.ToString()), res.ToString());             sw = Stopwatch.StartNew();             for (int i = 0; i < 100000; i++)             {                 res = ((float)param[0] * (float)param[1]) / 2 + 3 - 2 * 321;             }             sw.Stop();             Console.WriteLine(string.Format("Call : {0}\n", sw.Elapsed.ToString()), res.ToString());         }         float Test(float a, float b)         {             return (a * b) / 2 + 3 - 2 * 321;         }     }

 

 Code 저장 : 사용자가 작성한 Code를 저장하기 위해선 다른 Text파일로 저장 및 관리 해두는 방법과 Dll 파일의 EmbeddedResource 기능을 이용하는 방법이 있다.

컴파일시 ResourceWriter 개체를 이용해 Resource를 만들어서 사용자가 작성한 코드를 Resource 파일로 작성 후 Recource 파일을 어셈블리에 추가시키면 된다.

System.CodeDom.Compiler.CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = false;
parameters.IncludeDebugInformation = false;
parameters.GenerateInMemory = true;
 
System.Resources.ResourceWriter writer = new System.Resources.ResourceWriter("Resources.resx");
writer.AddResource("code.txt"Encoding.ASCII.GetBytes(code));
writer.Generate();
writer.Close();
parameters.EmbeddedResources.Add("Resources.resx");
 
 
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Core.dll");
parameters.ReferencedAssemblies.Add("System.Data.dll");
parameters.ReferencedAssemblies.Add("System.Drawing.dll");
parameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
 
parameters.OutputAssembly = DllName;
 
CompilerResults r = CodeDomProvider.CreateProvider("CSharp").CompileAssemblyFromSource(parameters, code);
if (r.Errors.HasErrors == true)
{
    CompilerError err = r.Errors[0];
    MessageBox.Show(string.Format("Line:{0}, Column:{1}, ErrorNumber{2}\r\nMessage:{3}"
       err.Line.ToString(), err.Column.ToString(), err.ErrorNumber.ToString(), err.ErrorText));
}

로드시에는 역으로 어셈블리에서 Resource를 검색 후 ResourceReader 개체를 이용해 Resource를 읽는다.

string code = string.Empty;
 
if (File.Exists(DllName))
{
    using (AssemblyHelper assem = new AssemblyHelper())
    {
        assem.LoadAssembly(Application.StartupPath + "\\" + DllName);
        code = assem.LoadCode();
    }
}
 
return code;

 

-Lambda

코드 형태의 문자열을 Lambda식으로 변경하는 기능은 .Net에서 제공해주지 않아서 직접구현하거나 오픈 소스를 이용해야한다. http://code.google.com/p/lambda-parser/ 오픈소스를 이용하면 응용 프로그램에 로드된 라이브러리는 모두 사용 할 수 있다. Parse 메서드는 String 형태의 Code와 파라미터의 타입 정보, 사용할 클래스의 NameSpace를 넘겨주면 Expression을 생성한다. 그 이후 Compile 메서드를 통해 Delegate를 생성한다.

 

string code = this.tbCode.Text.Trim().Split('=')[1];
if (string.IsNullOrEmpty(code))
    return;
code = this.tbCode.Text;
 
 
LambdaExpression ex = ExpressionParser.Parse(code, nullthis.parameters.ToArray(), 
           "System""System.Collections.Generic");
 
this.dele = ex.Compile();
 
this.func = (Func<floatfloatfloat>)dele;
 
MessageBox.Show("Success");

 

 

 

1.5 CodeDom, Lambda 테스트

테스트 결과

  컴파일 모드 : Release

테스트한 식 : (a * b) / 2 + 3 - 2 * 321

테스트 값 : float a = 2, float b = 3

호출 테스트 수 : 100,000

총 테스트 수: 6

 

CodeDom, Lambda 테스트 코드

Stopwatch sw = Stopwatch.StartNew();
 
float res = 0;
 
for (int i = 0; i < 100000; i++)
{
    if (param == null)
    {
        res = (float)this.dele.DynamicInvoke(null);
    }
    else
    {
        res = (float)this.dele.DynamicInvoke(param);
    }
}
sw.Stop();
 
Console.WriteLine(string.Format("DynamicInvoke : {0}", sw.Elapsed.ToString()), res.ToString());
 
 
sw = Stopwatch.StartNew();
 
for (int i = 0; i < 100000; i++)
{
    res = this.func((float)param[0], (float)param[1]);
}
 
sw.Stop();
 
Console.WriteLine(string.Format("FuncInvoke : {0}", sw.Elapsed.ToString()), res.ToString());
 
sw = Stopwatch.StartNew();
 
for (int i = 0; i < 100000; i++)
{
    res = ((float)param[0] * (float)param[1]) / 2 + 3 - 2 * 321;
}
 
sw.Stop();
 
Console.WriteLine(string.Format("Call : {0}\n", sw.Elapsed.ToString()), res.ToString());

 

 

CodeDom 결과 :

Dynamic

Func

CSharp

테스트1

0.0648687

0.0008836

0.0004886

테스트2

0.0615519

0.0008796

0.0004813

테스트3

0.0621032

0.0008879

0.0004833

테스트4

0.0618416

0.0008455

0.0004631

테스트5

0.0614814

0.0008925

0.0004909

테스트6

0.0619039

0.0008714

0.0004929

평균

0.062291783

0.00087675

0.00048335

Call 기준(배수)

128.8751078

1.813902969

1

1 CodeDom 테스트 결과

 

Lambda 결과 :

Dynamic

Func

CSharp

테스트1

0.0962379

0.0009402

0.0004741

테스트2

0.0890706

0.000969

0.0005181

테스트3

0.07660805

0.0009647

0.0005141

테스트4

0.065423

0.0010326

0.0004853

테스트5

0.0898079

0.0009634

0.0004621

테스트6

0.0904247

0.00097

0.000487

평균

0.084595358

0.000973317

0.000490117

CSharp 기준(배수)

172.6024926

1.985887714

1

2 Lambda 테스트 결과

1.6  결론

지금까지 CodeDom Lambda를 테스트 해보았다. 속도 테스트 결과 CodeDom이 약간 우세하였다. 내가 생각하는 장단점은 아래와 같다. 둘 중 하나를 선택하라면 Lambda식이 적합한 것 같다.

 

CodeDom의 장점은 변수의 선언 및 복잡한 식의 계산을 쉽게 할 수 있다는 것이다. 단점은 사용자가 C# 언어를 학습 해야 하고, 함수의 네이밍, 메모리 관리 등 개발이 복잡하다는 것이다.

Lambda의 장점은 간편한 식을 작성 할 수 있고, 사용자에게 친숙하고, 함수의 관리가 편하다. 단점은 변수의 선언 및 복잡한 식은 설정하기 어렵다.

 

프로젝트 : CalcDynamicCompile.zip 

 

 

1.   Math Expression Parser 비교

 

1.1 DataTable

.Net에서 제공하는 DataTable Class Compute 메서드를 이용해 식을 계산한 코드이다. 먼저 a, b 문자를 값으로 치환한 후 계산을 진행한다. Compute 메서드의 반환값은 object로써 type에 맞게 캐스팅 해주어야하는 단점이 있다. 테스트는 아래와 같이 진행하였다.

object res = null;
DataTable dt = new DataTable();
 
 
string exp = expression;
foreach (KeyValuePair<stringstring> item in values)
{
    exp = exp.Replace(item.Key, item.Value);
}
 
Stopwatch st = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
    res = dt.Compute(exp, null);
    Type type = res.GetType();
 
    if (type == typeof(int))
    {
        int a = (int)res;
    }
    else
    {
        double b = (double)res;
    }
}
st.Stop();
Console.WriteLine(string.Format("DataTable = {0} : {1}", st.Elapsed.ToString(), res.ToString()));

 

1.2 Ncalc

CodePlex에 등록되어 있는 오픈소스로써 System.Math 클래스의 메서들과 사용자 정의 함수를 지원한다. 사용자 정의 함수는 이벤트 방식으로 함수명으로 비교해서 개발자가 직접 처리를 해주어야 한다.

Debug.Assert(0 == new Expression("Sin(0)").Evaluate());

  Debug.Assert(2 == new Expression("Sqrt(4)").Evaluate());

Debug.Assert(0 == new Expression("Tan(0)").Evaluate());

 

//함수 처리

Expression e = new Expression("SecretOperation(3, 6)");

  e.EvaluateFunction += delegate(string name, FunctionArgs args)

      {

          if (name == "SecretOperation")

              args.Result= (int)args.Parameters[0].Evaluate() + (int)args.Parameters[1].Evaluate();

      };

  Debug.Assert(9 == e.Evaluate());

 

테스트는 아래와 같이 진행하였다.

object res = null;
NCalc.Expression ex = new NCalc.Expression(expression);
 
ex.Parameters.Clear();
 
Dictionary<stringobject> v = new Dictionary<stringobject>();
 
foreach (KeyValuePair<stringstring> item in values)
{
    ex.Parameters.Add(item.Key, Convert.ToSingle(item.Value));
}
 
Stopwatch st = Stopwatch.StartNew();
 
 
for (int i = 0; i < 100000; i++)
{
    res = ex.Evaluate();
}
 
st.Stop();
Console.WriteLine(string.Format("NCalc = {0} : {1}", st.Elapsed.ToString(), res.ToString())); 

 

1.3 Flee

CodePlex에 등록되어 있는 오픈 소스로써 Fast Lightweight Expression Evaluator라는 이름과 같이 테스트한 오픈소스 중에서는 가장 빠르다. System.Math Class 및 타 Class를 등록해서 사용 할 수있고, 사용자 정의 함수 기능을 제공한다. 사용자 정의 함수는 문자열로 정의 할 수 있지만, 매개변수가 바뀔시 Recalculate라는 메서드를 호출하여 엔진 내부를 수정 해야 하는 문제가 있다.

 

// 함수 테스트
CalculationEngine engine = new CalculationEngine();
ExpressionContext context = new ExpressionContext();
 
 
context.Variables.Add("a", 2);
context.Variables.Add("b", 2);
 
engine.Add("Func1""(a*b) + 2.0", context);
 
 
Stopwatch st = Stopwatch.StartNew();
double res = 0;
for (int i = 0; i < 800; i++)
{
    res = engine.GetResult<double>("Func1");
    context.Variables["a"] = 3;
    engine.Recalculate("Func1");
}
st.Stop();
Console.WriteLine(string.Format("FleeFunc = {0} : {1}", st.Elapsed.ToString(), res.ToString()));

 

테스트는 아래와 같이 진행하였다.

ExpressionContext context = new ExpressionContext();
 
double res = double.NaN;
 
context.Variables.Clear();
            
foreach (KeyValuePair<stringstring> item in values)
{
    context.Variables[item.Key] = Convert.ToDouble(item.Value);
}
 
IGenericExpression<double> exp = context.CompileGeneric<double>(expression);
 
Stopwatch st = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
    res = exp.Evaluate();
}
st.Stop();
Console.WriteLine(string.Format("Flee = {0} : {1}", st.Elapsed.ToString(), res.ToString()));

 

1.4 MathParseNet

CodeProject에 등록되어 있는 오픈 소스로써 sin, cos, tg, ctg 등 기본적인 수학 함수를 제공한다. 식의 해석과 동시에 값을 반환하는 형식이여서 오픈 소스 중에 가장 느렸고, 반환 값의 타입은 int, double 두개의 타입만 존재한다.

MathParserNet.Parser parser = new MathParserNet.Parser();
 
double res = double.NaN;
parser.RemoveAllVariables();
 
foreach (KeyValuePair<stringstring> item in values)
{
    parser.AddVariable(item.Key, item.Value);
}
 
Stopwatch st = Stopwatch.StartNew();
 
for (int i = 0; i < 100000; i++)
{
    MathParserNet.SimplificationReturnValue ret = parser.Simplify(expression);
    if (ret.ReturnType == MathParserNet.SimplificationReturnValue.ReturnTypes.Integer)
    {
        res = ret.IntValue;
    }
    else
    {
        res = ret.DoubleValue;
    }
}
st.Stop();
Console.WriteLine(string.Format("MathParseNet = {0} : {1}", st.Elapsed.ToString(),res.ToString()));

 

 

 

1.5  테스트

  컴파일 모드 : Release

테스트한 식 : (a * b) / 2 + 3 - 2 * 321

테스트 값 : float a = 2, float b = 3

호출 테스트 수 : 100,000

 

테스트 결과:

CSharp

Lambda

DataTable

Ncalc

Flee

MathParseNet

Test1

0.000352

0.000498

0.433453

0.205334

0.017759

14.45983

Test2

0.000259

0.0005

0.350152

0.204596

0.017735

14.61664

Test3

0.00028

0.000492

0.34059

0.154331

0.017455

14.54161

Test4

0.000243

0.000489

0.339338

0.154261

0.017464

14.52951

Test5

0.000251

0.000552

0.3525

0.206181

0.018056

14.5986

평균

0.000277

0.000506

0.363207

0.184941

0.017694

14.54924

CSharp 기준(배수)

1

1.827653

1311.215

667.656

63.87588

52524.33

3 Parser 테스트 결과

 

1.6 결론

Flee 오픈소스가 가장 빠르지만, CSharp으로 하드코딩한 연산에 비해 63배나 느렸다. Parser들은 속도에 민간한 프로젝트에 적용하기는 힘들 것 같다.

 

 

프로젝트 :AboutCalculParser.zip

 

 

+ Recent posts