안녕하세요? 맨날맑음입니다.

닷넷의 리플렉션(Reflection)을 이용하면 어셈블리의 메터데이터 정보를 알아올 수 있고, 이를 이용해 지연바인딩(Lazy Binding)이나 동적으로 코드를 생성할 수 있습니다. 이번 포스팅에서는 리플렉션을 이용한 동적 코드생성에 대해 알아 보도록 하겠습니다.

 

간단한 예제를 통해 동적 코드생성에 대해 알아 볼 텐데요. 예제는 어셈블리를 동적으로 생성/실행하는 콘솔어플리케이션 프로젝트(App)와, 동적으로 코드를 생성하는 클래스 라이브러리 프로젝트(CodeGenerator)로 구성 됩니다.

CodeGenerator 클래스에서는 동적으로 DynamicAssembly를 생성하고, DynamicClass, DynamicMethod를 만들어 그 안에 "Dynamic Method Call"이라는 메시지를 프린트하는 코드를 작성하게 만들도록 하겠습니다.

 

  1. 파일 -> 새로 만들기 -> 프로젝트를 선택하여 빈 솔루션을 생성합니다.
  2. 솔루션 탐색기의 솔루션 탭을 마우스 오른쪽 클릭하여, 콘솔어플리케이션 프로젝트(App)와 클래스라이브러리 프로젝트(CodeGenerator)를 생성합니다.
  3. Programe.cs 와 Class1.cs의 이름을 각각 App와 CodeGenerator로 변경합니다.

#1. CodeGenerator 클래스 작성

CodeGenerator.cs에 다음의 코드를 입력합니다.

public class CodeGenerator
{
    public Type DynamicType { getprivate set; }
    public CodeGenerator()
    {
        //현재 어플리케이션 도매인을 가져온다.
        AppDomain currentDomain = AppDomain.CurrentDomain;
        //생성하려는 어셈블리의 이름을 설정한다.
        AssemblyName assemName = new AssemblyName("DynamicAssembly");         //어셈블리를 생성         AssemblyBuilder assemBuilder = currentDomain.DefineDynamicAssembly(assemName, AssemblyBuilderAccess.Run);
        //모듈 생성         ModuleBuilder moduleBuilder = assemBuilder.DefineDynamicModule("DynamicModule");         //클래스 생성         TypeBuilder typeBuilder = moduleBuilder.DefineType("DynamicClass"TypeAttributes.Public);         //메서드 생성         MethodBuilder methodBuilder = typeBuilder.DefineMethod("DynamicMethod"MethodAttributes.Public, nullnull);         //동적 코드 생성         ILGenerator msil = methodBuilder.GetILGenerator();         msil.EmitWriteLine("Dynamic Method Call");         msil.Emit(OpCodes.Ret);         //타입 정보 저장     DynamicType = typeBuilder.CreateType();     }
}

 

DynamicType이라는 필드가 있고, 이 필드는 동적으로 생성 할 Class의 Type을 저장하는 용도로 쓰입니다. 생성자에서는 동적으로 코드를 생성하기 위한 단계를 진행하는데요. 어셈블리->모듈->클래스->메서드->코드 생성의 단계로 진행을 하게 됩니다. 마지막에 보이는 ILGenerator 클래스를 이용하여 msil코드를 생성 할 수 있게 됩니다. 예제에서는 간단히 EmitWriteLine 메서드를 이용하여 "Dynamic Method Call"이라는 문자열을 화면에 뿌리는 코드를 생성 하였습니다.

 

#2. App 클래스 작성

App프로젝트로에서 CodeGenerator 프로젝트를 참조 추가 합니다.


				

App.cs 에 다음의 코드를 추가합니다.

class App
{
    static void Main(string[] args)
    {
        //CodeGenerator클래스 생성
        CodeGenerator.CodeGenerator codeGenerator = new CodeGenerator.CodeGenerator();
        //동적으로 추가 한 Type을 얻어옴
        Type DynamicType = codeGenerator.DynamicType;
        //Activator 클래스를 이용하여 인스턴스 생성
        object obj = Activator.CreateInstance(DynamicType);
        //동적으로 생성한 메소드를 얻어옴
        MethodInfo methodInfo = DynamicType.GetMethod("DynamicMethod");
        //실행
        methodInfo.Invoke(obj, null);
    } }

Main메서드의 로직은 간단합니다. 우선 CodeGenerator 클래스의 public 필드인 DynamicType에서 동적으로 생성한 Class의 Type을 얻어옵니다. Activator 클래스를 이용하여 클래스의 인스턴스를 생성하고, MethodInfo 클래스로 미리 생성한 DynamicMethod를 얻어와 실행합니다.

 

실행결과는 Dynamic Method Call이 나오면 의도한 결과이겠죠?

 


				

이상 동적으로 코드를 생성하고, Reflection을 이용하여 동적으로  인스턴스를 생성 / 실행하는 방법이었습니다. 

감사합니다.

 

소스코드 빌드 환경 : Windows7 x64, Visual Studio2010, .NET Framwork4.0

Posted by 맨날맑음
,
안녕하세요? 맨날맑음 입니다.

정말 오랜만에 블로깅이네요. 3개월 정도 지난거 같네요. 바쁘다는 핑계로 블로깅도 미루고, 덕분에 블로깅 할 주제는 많이 생겨서 좋은건가요?

- Fig1. 곰플레이어에서 파일 Drag&Drop을 이용하여 실행시키는 장면 -

 
많은 어플리케이션이 위와같이 파일을 Drag&Drop만으로 열 수 있는 기능을 제공해 주고있습니다. 실제 파일을 다루는 어플리케이션을 개발하다보면 빠지면 서운한 기능일텐데요. .Net WinForm에서는 아주 쉬운방법으로 위와 같은 기능을 구현 할 수 있습니다.

#1. Windows Forms 프로젝트 생성.

- Fig2. Windows Forms 프로젝트 생성 -


#2. ListBox 추가

- Fig3. Form에 ListBox를 추가한 모습 -

여기서 ListBox는 Form위로 드레그 되어 들어온 파일의 정보를 보기위한 간단한 용도입니다.

#3. 드레그 드롭 관련 지식
public virtual bool AllowDrop { get; set; }
- 사용자가 컨트롤로 끌어 온 데이터가  컨트롤에서 허용되는지 여부를 나타내는 값을 가져오거나 설정.
- 반환 값: 끌어서 놓기 작업을 수행할 수 있으면 true이고, 그렇지 않으면 false, 기본값은 false


Controls의 AllowDrop 프로퍼티를 이용하면 Control에 Drag로 데이터를 끌어올 수 있는지 없는지 설정 할 수 있습니다. 이말은 즉 Controls를 상속받는 많은 WinForm과 Control들이 AllowDrop 프로퍼티를 사용 할 수 있다는 말입니다.

Drag&Drop 관련 이벤트
Controls.DragDrop
: 드레그 작업이 완료되면 발생.
Controls.DragOver : 드레그한 개체가 컨트롤 위로 올라오면 발생.
Controls.DragEnter : 드레그한 개체가 컨트롤 범위 안으로 들어오면 발생.
Controls.DragLeave : 드레그한 개체가 컨트롤 범위 밖으로 나가면 발생.

4개의 이벤트가 있는데요. 우리가 사용 할 이벤트는 DragDrop과 DragOver 두개 입니다.

DragEventHandlerDragEventArgs
X : 화면 좌표로 나타난 마우스 포인터의 x좌표.
Y : 화면 좌표로 나타난 마우스 포인터의 Y좌표.
Data.GetDataPresent : 인스턴스에 저장된 데이터가 지정된 형식과 관련 있는지 확인.
Effect : 드레그한 개체에대한 마우스 커서를 결정.

이벤트관련 클래스의 사용법도 알아보았으니 이제 구현을 해봅니다!

#4. 코드 구현

using System.Windows.Forms;

 

namespace DragSample

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

            //드레그를 허용

            this.AllowDrop = true;

            //Drag관련이벤트 연결------------------------------------------------

            this.DragOver += new DragEventHandler(Form1_DragOver);

            this.DragDrop+=new DragEventHandler(Form1_DragDrop);           

        }

 

        //드레그한 개체가 폼위로 올라올때--------------------------------------

        void Form1_DragOver(object sender, DragEventArgs e)

        {

            //드레그하는 개체가 파일이면

            if (e.Data.GetDataPresent(DataFormats.FileDrop))

            {

                //마우스 커서를 Copy모양으로 바꿔준다.

                e.Effect = DragDropEffects.Copy;

            }

            else

            {

                //아닐경우 커서의 모양을 θ 요런 모양으로 바꾼다.

                e.Effect = DragDropEffects.None;

            }

        }

 

        //드레그한 개체를 폼위에 올려 놓았을때----------------------------------

        void Form1_DragDrop(object sender, DragEventArgs e)

        {

            //객체들의 이름을 string 배열에 얻어온다.

            string[] files = e.Data.GetData(DataFormats.FileDrop) as string[];

            if(files!=null)

            {

                foreach (string file in files)

                {

                    //끌어온 파일명을 리스트박스에 달아준다.

                    listBox1.Items.Add(file);

                }               

            }

        }

    }

}

간단한 소스라 따로 설명은 없어도 될 것 같습니다. 유의 할 점은 e.Data.GetData(DataFormats.FileDrop)에서 반환값이 string 배열이라는 점입니다.

이와같은 방법을 응용하면 ListBox간 아이템 이동이라던지, 대부분의 Drag&Drop 기능은 구현 가능 할 것 같습니다.
Posted by 맨날맑음
,

안녕하세요? 맨날맑음입니다.

화살표를 그려야 할 일이 생겨서 어떻게 그릴까 생각하다가, Line을 여러개 그려서 화살표를 만들려고 했습니다.
선의 각도에 따라 화살표 머리 부분의 각도가 변하기 때문에 수학을 못하는 저로써는 화살표 그리는 것 조차도 힘든일이 었습니다.

하지만! .NET의 Pen 클래스는 Line의 끝점들의 모양을 변경시켜 주는 기능을 제공합니다.
Pen 클래스의 좀더 자세한 설명은 아래의 MSDN 도움말을 참조 하시면 됩니다.
http://msdn.microsoft.com/ko-kr/library/system.drawing.pen(en-us,VS.85).aspx

MSDN에서 Pen 클래스의 속성을 보면 StartCap 과 EndCap이 있습니다. 이것이 선의 끝모양을 지정하는 속성입니다. 그럼 선의 시작과 끝 모양을 화살표로 바꾸어 보겠습니다.

Pen pen = new Pen(Color.Blue, 6);
pen.StartCap = LineCap.ArrowAnchor;
pen.EndCap = LineCap.ArrowAnchor;
위의 소스에서 처럼 Pen 객체를 생성하고, 해당하는Cap 속성을 바꿔주는 아주 간단한 방법으로 끝모양을 바꿔줄 수 있습니다.  LineCap 사용을 위해서는 using System.Drawing.Drawing2D;을 해주셔야 합니다.
화살표 모양 이외에도 여러 모양을 지정 할 수 있습니다. LineCap 을 살펴 보면 열거형으로 되어 있습니다. 각각의 멤버의 의미를 보면 다음과 같습니다.
Flat : 일직선 형태의 끝 모양을 지정합니다.
Square : 정사각형 형태의 선 끝 모양을 지정합니다.
Round : 둥근 선 끝 모양을 지정합니다.
Triangle : 삼각형 선 끝 모양을 지정합니다.
NoAnchor : 앵커를 지정하지 않습니다.
SquareAnchor : 정사각형 앵커 선 끝 모양을 지정합니다.
RoundAnchor : 둥근 앵커 끝 모양을 지정합니다.
DiamondAnchor : 다이아몬드 앵커 끝 모양을 지정합니다.

ArrowAnchor : 화살표 모양의 앵커를 지정합니다.
Custom : 사용자 지정 선 끝 모양을 지정합니다.

AnchorMask : 선 끝 모양이 앵커 모양인지 여부를 검사하는 데 사용되는 마스크를 지정합니다.
아래와 같이 간단하게 Windows Form 프로젝트를 생성해서 Paint 이벤트 핸들러에서 화살표를 그려 보겠습니다.

using System.Drawing;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.Paint += new PaintEventHandler(Form1_Paint);
        }

        void Form1_Paint(object sender, PaintEventArgs e)
        {
            Pen pen = new Pen(Color.Blue, 6); //Pen 객체 생성
            pen.StartCap = LineCap.ArrowAnchor; //Line의 시작점 모양 변경
            pen.EndCap = LineCap.ArrowAnchor; //Line의 끝점 모양 변경
            e.Graphics.DrawLine(pen, 20, 50, 300,50); //Line 그리기
        }
    }
}
 
이와같이 복잡한 화살표 알고리즘 없이 Pen 클래스의 속성 변경 만으로도 간단하게 화살표를 그릴 수 있습니다.
참 쉽죠잉~
Posted by 맨날맑음
,

안녕하세요? 맨날맑음 입니다.

회원가입 기능을 구현하다 보면, 주민등록번호가 올바른지 체크해야 하는 경우가 생기게 됩니다.
우선 주민등록번호가 어떠한 형태(?)로 이루어 졌는지 알아 볼 필요가 있습니다.

주민등록번호는 총 13자리 숫자로 구성되어 있습니다.
모두 아시는 바와같이 앞의 6자리는 태어난 날의 년, 월, 일을 나타냅니다  
뒤의 7자리는 조금 복잡한데요. 
 # 1번 자리 : 성별 (ex. 남자 : 1또는3, 여자 : 2또는4)
    - 3과 4는 낮설겠지만 00년 이후 출생자부터는 남자는 3 여자는 4입니다.
 # 2~5번 자리 : 출생 신고 당시의 거주지 관할 동사무소의 지역코드
 # 6번 자리 : 출생 신고 날짜
 # 7번 자리 : 검정 코드
    - 7번 자리가 매우 중요합니다. 이것은 앞의 12자리의 유효성을 검증해 줄 매직넘버(?)로 쓰이게 됩니다. 

주민등록번호의 유효성을 알아 보기 위해서는 각 자리수의 미리 지정된 값을 곱하여, 곱한 결과를 전부 더하고,
그 값을 11로 나눈 후, 그 결과 값(나머지)11에서 빼어 13번째 자리수인 검정 코드와 일치하면 유효하게 됩니다.
참.. 말로 할려니 쉽지 않습니다.

주민등록번호가 123456 - 1234567 이라고 예를 들어 보겠습니다.
# 1. 아래와 같이 검정 코드인 7을 제외한 각자리에 미리 지정된 값(234567892345)을 각각 곱하여 줍니다.

# 2. 각 자리에서 곱한 결과를 모두 더해 줍니다.

# 3. 더한 결과(206)을 11로 나누어 줍니다(% 나머지 연산).

# 4. 11에서 나머지의 결과를 빼줍니다.

# 5. 나머지 3과 검증코드 7이 같은지 확인 합니다. 같으면 유효한 주민등록번호입니다. 이 경우 11에서 10을 뺀다던지, 1을 빼면 2자리의 결과가 나오게 됩니다. 그럴때는 뒤의 1자리만 취하여 비교하시면 됩니다. (이와같은 이유로 4번의 결과에 % 연산으로 10을 한번 더해주면 뒤의 1자리만 취할 수 있겠죠?)

# 결론 . 주민등록번호 123456 - 1234567는 틀린 주민번호 입니다!


C# 코드로 만들어 보겠습니다.
코드 작성시 고려 사항
  # 1. 주민등록번호가 13자리 인가?
  # 2. 13자리 모두 숫자인가?
  # 3. 위의 공식에 부합하는가?

bool IsAvailableRRN(string RRN)

{

    //공백 제거

    RRN = RRN.Replace(" ", "");

    //문자 '-' 제거

    RRN = RRN.Replace("-", "");

    //주민등록번호가 13자리인가?

    if (RRN.Length != 13)

    {

        return false;

    }

 

    int sum = 0;

    for (int i = 0; i < RRN.Length - 1; i++)

    {

        char c = RRN[i];

        //숫자로 이루어져 있는가?

        if (!char.IsNumber(c))

        {

            return false;

        }

        else

        {

            if (i < RRN.Length)

            {

                //지정된 숫자로 자리를 나눈 더한다.

                sum += int.Parse(c.ToString()) * ((i % 8) + 2);

            }

        }

    }

    // 검증코드와 결과 값이 같은가?

    if (!((((11 - (sum % 11)) % 10).ToString()) == ((RRN[RRN.Length - 1]).ToString())))

    {

        return false;

    }

    return true;

}

Posted by 맨날맑음
,
안녕하세요? 맨날맑음 입니다.

몇몇 프로그램을 보면, 윈도우끼리 도킹 기능으로 서로 일정거리 만큼 가까워지면 딱 달라붙게 만들어진 것을 볼 수 있습니다. 이런 기능의 명칭을 정확히 몰라서; 일단 제목은 '자석 윈도우'라고 정해 보았는데요;

밑의 예(이스트소프트의 알송)에서 볼 수 있듯이 각 윈도우끼리는 가까이 가면 달라 붙고, 제일 위의 윈도우를 이동하면 붙어있는 윈도우도 따라서 이동하게 됩니다.
이런 기능을 간단하게 Windows Form을 이용하여 만드는 방법을 알아보려 합니다. 이런 기능을 제공하는 Class Library가 있을까 찾아 보다가 결국 찾지는 못하고, 한 외국 사이트에 이와같은 소스를 발견하였습니다. 현재 출처를 정확하게 기억 나지 않습니다. 그래서 완성된 소스는 올리지 않습니다.(필요하신분은 요청하세요)
Form1.cs
 private void Form1_Move(object sender, EventArgs e)
        {
            if (isForm2Docked)
            {
                tempForm2.SetDesktopLocation(tempForm2.Location.X + (this.Location.X - prevLoc.X),
                    tempForm2.Location.Y + (this.Location.Y - prevLoc.Y));
            }
            prevLoc.X = this.Location.X;
            prevLoc.Y = this.Location.Y;
        }
 
메인이 되는 폼의 Move 이벤트에서는 현재 서브폼이 붙어있는지 확인하여, 붙어있으면 위치를 함께 옮겨줍니다.

Form2.cs
private void Form2_Move(object sender, EventArgs e)
        {
            bool amIDocked = false;
            Point newPoint = GetNewFormPosition(this, passedInForm, out amIDocked);
            this.SetDesktopLocation(newPoint.X, newPoint.Y);
            ((Form1)passedInForm).isForm2Docked = amIDocked;
        }

        public Point GetNewFormPosition(Form thisForm, Form parentForm, out bool isDocked)
        {
            int[] xRange = new int[2] { parentForm.Location.X, parentForm.Location.X + parentForm.Width };
            int[] yRange = new int[2] { parentForm.Location.Y, parentForm.Location.Y + parentForm.Height };
            int xGap = Math.Abs(thisForm.Location.X - parentForm.Location.X);
            int yGap = Math.Abs(thisForm.Location.Y - parentForm.Location.Y);
            int leftGap = Math.Abs((thisForm.Location.X + thisForm.Width) - parentForm.Location.X);
            int rightGap = Math.Abs(thisForm.Location.X - (parentForm.Location.X + parentForm.Width));
            int topGap = Math.Abs((thisForm.Location.Y + thisForm.Height) - parentForm.Location.Y);
            int bottomGap = Math.Abs(thisForm.Location.Y - (parentForm.Location.Y + parentForm.Height));
            int xNew = thisForm.Location.X;
            int yNew = thisForm.Location.Y;
            isDocked = false;

            if ((leftGap <= dockGap) && (((thisForm.Location.Y >= yRange[0]) && (thisForm.Location.Y <= yRange[1])) ||
                ((thisForm.Location.Y + parentForm.Height >= yRange[0]) && (thisForm.Location.Y + parentForm.Height <= yRange[1]))))
            {
                xNew = parentForm.Location.X - thisForm.Width;
                if (yGap <= dockGap) yNew = parentForm.Location.Y;
                isDocked = true;
            }
            if ((rightGap <= dockGap) && (((thisForm.Location.Y >= yRange[0]) && (thisForm.Location.Y <= yRange[1])) ||
                ((thisForm.Location.Y + parentForm.Height >= yRange[0]) && (thisForm.Location.Y + parentForm.Height <= yRange[1]))))
            {
                xNew = parentForm.Location.X + parentForm.Width;
                if (yGap <= dockGap) yNew = parentForm.Location.Y;
                isDocked = true;
            }
            if ((topGap <= dockGap) && (((thisForm.Location.X >= xRange[0]) && (thisForm.Location.X <= xRange[1])) ||
                ((thisForm.Location.X + parentForm.Width >= xRange[0]) && (thisForm.Location.X + parentForm.Width <= xRange[1]))))
            {
                yNew = parentForm.Location.Y - thisForm.Height;
                if (xGap <= dockGap) xNew = parentForm.Location.X;
                isDocked = true;
            }
            if ((bottomGap <= dockGap) && (((thisForm.Location.X >= xRange[0]) && (thisForm.Location.X <= xRange[1])) ||
                ((thisForm.Location.X + parentForm.Width >= xRange[0]) && (thisForm.Location.X + parentForm.Width <= xRange[1]))))
            {
                yNew = parentForm.Location.Y + parentForm.Height;
                if (xGap <= dockGap) xNew = parentForm.Location.X;
                isDocked = true;
            }
            return new Point(xNew, yNew);
        }

서브폼에서 좀 복잡해 보이긴 하지만, 메인폼의 영역을 알아와, 현재 서브폼의 위치가 달라 붙어야하는 위치라면 붙여 주고, 메인폼에 그 사실을 알리는 역할을 합니다.
이 소스를 응용하면, 상용프로그램처럼 윈도우간 도킹 기능을 만드는 것도 어렵지 않을 것 같습니다.

ps. 좌표를 일일이 비교해가며 구현하는 방법 외에 Class Library나 다른 방법을 아시는 분은 꼭 피드백을 부탁 드립니다!!
Posted by 맨날맑음
,

안녕하세요? 맨날맑음 입니다.

윈도우에서 [Ctrl + Alt + Del]키를 누르면 아래와 같이 Windows 작업관리자를 볼 수 있습니다. 응용 프로그램 탭을 보면 현재 실행 중인 프로그램의 목록을 확인 할 수 있는데요. 이와같이 프로그램을 만들다 보면 특정한 프로그램을 선택하는 창을 제공해야 할 때가 있습니다.
이런 기능을 만들기 위해서는 Win32 API의 도움을 받아야합니다.(.net class library에서 지원해 주는지는 잘 모르겠습니다. 아시는분은 꼭 알려주시길..) 잡설이지만 .NET의 Windows Form 개발시 자꾸만 Win32 API를 사용하게 되는군요'' 분명 그에 매칭하는 .NET Class가 있을꺼라는 생각이 드는데요(일부 크리티컬한 기능 제외);

Win32 API의 EnumWindows 함수는 현재 실행중인 프로세서들의 목록을 얻어올 수 있는 기능을 제공합니다.
http://msdn.microsoft.com/en-us/library/ms633497(VS.85).aspx 이 링크에서 보다 자세한 MSDN 도움말을 확인 할 수있습니다.

C#에서 사용시에는 다음과 같이 선언 해 주어야 하는데, 한가지 고민되는 사항은 첫번째 인자가 callback 함수의 '함수포인터'를 넣어야 되는 것입니다. 두번째 인자의 의미는 사용자 임의의 데이터를 넘길 때 사용합니다.
[DllImport("user32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public static extern bool EnumWindows(EnumWindowsCallback callback, int extraData);

그런데 C#에는 함수포인터가 없기 때문에 이를 대체하기 위한 수단으로 'Delegate'를 사용해야 합니다.

그래서 아래과 같이 EnumWindows의 Callback 함수와 형식이 일치하는 Delegate를 선언해 줍니다.
public delegate bool EnumWindowCallback(int hwnd, int lParam);
배웠으니 시험도 해볼겸 간단한 예제를 만들어 보겠습니다. Windows Form 프로젝트를 생성합니다.
Form위에 프로그램의 목록과 아이콘을 띄워줄 ListView를 가져다 놓습니다.

각자 입맛에 맛게 속성을 조정해 줍니다. 저는 Form크기에 맞게 딱 붙도록 Dock속성을 Fill로 변경해 보았습니다.
코드(form.cs)로 가서 아래의 코드를 추가합니다.
using System;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace Sample
{
    public partial class Form1 : Form
    {
        public delegate bool EnumWindowCallback(int hwnd, int lParam);
        
        [DllImport("user32.dll")]
        public static extern int EnumWindows(EnumWindowCallback callback, int y);

        [DllImport("user32.dll")]
        public static extern int GetParent(int hWnd);

        [DllImport("user32.dll")]
        public static extern int GetWindowText(int hWnd, StringBuilder text, int count);

        [DllImport("user32.dll")]
        public static extern long GetWindowLong(int hWnd, int nIndex);

        [DllImport("user32.dll")]
        public static extern IntPtr GetClassLong(IntPtr hwnd, int nIndex);

        const int GCL_HICON = -14; //GetWindowLong을 호출할 때 쓸 인자
        const int GCL_HMODULE = -16;
        ImageList imgList;//ListView의 Image로 쓸 리스트
               

        public Form1()
        {
            InitializeComponent();
            imgList = new ImageList();
            imgList.ImageSize = new Size(16, 16);
            listView1.SmallImageList = imgList;
            listView1.View = View.List;
            //CallBack 델리게이트 생성
            EnumWindowCallback callback = new EnumWindowCallback(EnumWindowsProc);
            EnumWindows(callback, 0);

        }
       
        public bool EnumWindowsProc(int hWnd, int lParam)
        {            
            //윈도우 핸들로 그 윈도우의 스타일을 얻어옴
            UInt32 style = (UInt32)GetWindowLong(hWnd, GCL_HMODULE);
            //해당 윈도우의 캡션이 존재하는지 확인
            if ((style & 0x10000000L) == 0x10000000L && (style & 0x00C00000L) == 0x00C00000L)
            {
                //부모가 바탕화면인지 확인
                if (GetParent(hWnd) == 0)
                {
                    StringBuilder Buf = new StringBuilder(256);
                    //응용프로그램의 이름을 얻어온다
                    if (GetWindowText(hWnd, Buf, 256) > 0)
                    {
                        try
                        {
                            //HICON 아이콘 핸들을 얻어온다
                            IntPtr hIcon = GetClassLong((IntPtr)hWnd, GCL_HICON);
                            //아이콘 핸들로 Icon 객체를 만든다
                            Icon icon = Icon.FromHandle(hIcon);
                            imgList.Images.Add(icon);                            
                        }
                        catch (Exception)
                        {
                            //예외의 경우는 자기 자신의 윈도우인 경우이다.
                            imgList.Images.Add(this.Icon);
                        }
                        listView1.Items.Add(new ListViewItem(Buf.ToString(), listView1.Items.Count));
                    }
                }
            }
            return true;
        }    
    }
}

소스가 좀 많아 보이긴 하는데, 복잡하진 않습니다. Win32API들을 사용하기 위해 EnumWindows, GetParent, GetWindowText, GetWindowLong, GetClassLong을 선언해 줍니다.
ListView의 Image를 넣기 위한 ImageList도 추가합니다.

생성자에서는 ImageListListView의 초기화를 하고, Delegete를 생성하여, EnumWindows를 호출해 줍니다.
그럼 OS는 현재 실행중인 프로세서를 EnumWindowsProc으로 전달해 주게 됩니다. 단 EnumWindowsProc으로 전달되는 것은 프로세스 목록(전체)이 아니라 한번에 하나씩 들어오게 됩니다.

EnumWindowsProc에서는 첫번째 매개변수로 들어온 윈도우 핸들을 이용하여 GetWindowLong을 호출해 윈도우의 스타일을 얻어오고, 해당윈도우의 캡션이 존재하는지 확인합니다.

그 다음으로 부모가 바탕화면인지 확인하여, 이것이 최상위의 윈도우인지 확인하게 됩니다. GetWindowText함수를 이용하여, 윈도우(응용프로그램)의 이름을 얻어온후, GetClassLong을 사용하여 해당 윈도우의 HICON 아이콘핸들을 얻어오게 됩니다(.net에서 사용하기 위해 IntPtr 사용), 이렇게 얻어온 아이콘핸들을 이용하여 .net의 Icon 클래스의 static메소드인 FromHandle을 이용하여 Icon 객체를 생성합니다.

예외처리 구문은 자기 자신의 윈도우일 경우 Icon을 못얻어오는 경우가 생겨서, try를 사용하였습니다.
EnumWindowsProc의 return은 true로 해주셔야, 계속하여 호출하게 됩니다. 상황에따라 원하는 윈도우를 찾은다음에 false를 return하면 되겠습니다. 실행시켜 보니 잘되는군요!
사실, API의 EnumWindows를 사용하지 않고 .net의 Process 클래스를 사용해도 현재 사용중인 프로세스의 목록을 얻어, 바탕화면이 부모인지 확인하고 MainWindowTitle을 확인하여 이와같은 기능을 만들 수 도 있습니다.(하지만 탐색기 같은 일부창의 경우 윈도우의 이름을 얻어올 수 없는 문제가 있습니다.) 아래는 Process를 이용하는 간단한 예제입니다.
Process[] pro = Process.GetProcesses();            
for (int i = 0; i < pro.Length; i++)
{
     if (pro[i].MainWindowHandle != IntPtr.Zero)
     {
          if (pro[i].MainWindowTitle == "")
              continue;
       //To do..
     }
}
Posted by 맨날맑음
,

안녕하세요? 맨날맑음 입니다.

네이트온을 사용하다 보면 친구가 로그인 했더나 대화 요청이 들어올때 트레이 아이콘 주위에 알림창이 뜨는것을 볼 수  있습니다.

이와 같이 트레이창의 위치를 윈도우 주위로 위치하는 것은 .NET에서도 Win32API의 SystemParamete
rsInfo
를 사용하면 간단하게 구현 할 수 있습니다.

.NET에서 Win32API를 사용하기위해 우선 using System.Runtime.InteropServices; 이 필요합니다.
또한 SystemParametersInfo에 대한 아래와 같은 선언도 필요합니다.
 [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int SystemParametersInfo(int uAction, int uParam, out RECT lpvParam, int fuWinIni);


SystemParametersInfo 함수를 호출하여 작업표시줄의 영역을 뺀 화면의 크기를 얻어 오기 위하여 RECT 구조체가 필요 하기 때문에 아래와 같이 임의로 RECT 구조체를 정의해 줍니다.
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
        public int left;
        public int top;
        public int right;
        public int bottom;
}
사실 SystemParametersInfo는 매우 다양한 기능을 수행하는 API입니다. 여러가지 시스템 정보를 설정하거나 가져 올 수 있습니다. 예를들어 바탕화면 이미지를 변경한다던지, 작업영역의 크기를 알아낼 수 도 있고, 화면보호기의 동작 유무를 제어 할 수도 있습니다. 인자에 따른 기능이 너무 많아서 좀더 자세한 기능을 알고 싶다면 아래의 링크에서 MSDN 도움말을 확인 하시면 됩니다.
http://msdn.microsoft.com/en-us/library/ms724947(VS.85).aspx

위에 그림에서 보듯이 작업표시줄의 트레이 아이콘 위에 윈도우를 띄우려면(무언가 보여주고 싶다면...) 작업표시줄의 영역을 제외한 윈도우의 실질적인 작업영역의 크기를 얻어오면 됩니다.
        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern int SystemParametersInfo(int uAction, int uParam, out RECT lpvParam, int fuWinIni);

        private void SetFormLocationToTray(Form form)
        {
            int SPI_GETWORKAREA = 0x0030; //작업영역을 알아오는 Flag
            RECT r = new RECT();
            SystemParametersInfo(SPI_GETWORKAREA, 0, out r, 0);
            Size s = form.Size;
            Point p = new Point(r.right - s.Width, r.bottom - s.Height); 
            form.Location = p;
        }
 
SetFormLocationToTray 함수는 인자로 들어온 Form 객체의 위치를 Tray 주위로 옮겨 주는 역할을 합니다.
SystemParametersInfo의 첫번째 인자중 0x0030(SPI_GETWORKAREA)을 넣으면 작업표시중을 뺀 작업영역의 크기를 3번째 인자의 RECT 구조체에 얻어오게 됩니다.
알아온 작업영역의 크기로 위의 소스에서 보는것 처럼 현재 폼의 크기에 따라 계산을 하여 폼의 location을 변경하여 네이트온처럼 트레이 주위에 윈도우가 뜨게 할 수 있습니다.
Posted by 맨날맑음
,
안녕하세요? 맨날맑음 입니다.

어플리케이션을 개발 하다 보면 같은 프로그램이 두번 실행 되지 않아야 하는 경우가 종종 생깁니다.
이를 해결 하기 위해 여러 방법이 있지만.. 이번 포스팅에 소개 할 내용은 커널 동기화 객체 중 하나인 뮤텍스(Mutex)를 이용하여 해결하는 방법을 소개하려 합니다.

Mutex는 .NET의 System.Threding 네이스페이스에 포함된 클래스 입니다. 사실 둘이상의 스레드가 동시에 공유될 수 있는 리소스에 접근 할때 데드락의 위험이 있으므로 동기화 매커니즘이 필요하게 되는데요. Mutex는 리소스에 대한 단독 엑세스 권한을 하나의 스레드에만 부여하여 동기화하는 기본형식 입니다.
같은 리소스에 접근 할때 하나의 스레드가 Mutex를 걸고 어떠한 일을 수행 할 경우 다른 스레드는 먼저 수행하고 있는 스레드가 Mutex를 해제 할때까지 기다리게(일시중지) 되게 됩니다.
Mutex에 관한 자세한 설명은 아래의 링크에서 MSDN 도움말을 확인 할 수 있습니다.
http://msdn.microsoft.com/ko-kr/library/system.threading.mutex.mutex.aspx

테스트 하는 어플리케이션으로는 Windows Form 프로젝트를 예로 들겠습니다.
WinForm 프로젝트를 생성하면 Program.cs 가 생성 됩니다.
Main 함수에 다음과 같은 코드를 추가 시켜 주면 메세지 막스가 뜨면서 해당하는 프로그램이 중복 실행 되었다는걸 알릴 수 있습니다. 매우 간단하죠?
static class Program
    {
        /// 
        /// 해당 응용 프로그램의 주 진입점입니다.
        /// 
        [STAThread]
        static void Main()
        {
            bool createdNew;
            Mutex dup = new Mutex(true, "Focus Explorer Mutex", out createdNew); //Mutex생성
            if (createdNew)
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new MainForm());
                dup.ReleaseMutex(); //Mutex 해제
            }
            else // 프로그램이 중복 실행된 경우
            {
                MessageBox.Show("이미 프로그램이 실행중입니다.","Focus Explorer");
            }
        }
    }
 
우선 Mutex를 생성하고, 생성자에 두번째 인자에 뮤텍스 이름을 지정합니다.
세번째 인자로준 bool 값이 false일 경우 이미 뮤텍스가 걸리있는 상태이기 때문에.. 해당 프로젝트가 이미 생성되었다고 생각하고 프로그램이 실행중이라는 메세지 박스를 띄우고, 프로그램을 종료합니다.

이상 간단한 Mutex를 사용한 프로그램 중복 방지 방법이었습니다.
Posted by 맨날맑음
,
안녕하세요? 맨날맑음 입니다.

.NET 으로 프로젝트를 만들고, 배포를 하는 방법은 Click Once같은 방법도 있지만 이번에는 Windows Installer 배포에 대해 알아보겠습니다. 기존에 .Net으로 프로젝트 개발만 해보았지, 배포는 신경을 쓰지 않아 잘 모르다가, 이번에 VS2008에서 기본으로 제공되는 배포 프로젝트의 사용법을 알아 보았습니다.
아래 링크를 따라가면 배포프로젝트에 관한 MSDN 도움말을 보실 수 있습니다.
http://msdn.microsoft.com/ko-kr/library/206sadcd(VS.80).aspx

우선 배포할 프로그램을 준비합니다. 저는 간단한 윈폼 프로젝트로 하겠습니다.

VS2008에서 새 프로젝트를 추가 하고, 기타 프로젝트 형식 -> 설치 및 배포 -> 설치프로젝트를 선택합니다.

다음과 같이 배포 프로젝트가 생성됩니다.

솔루션 탐색기와 속성 창을 보면 아래와 같이 프로젝트와, 여러 속성이 보이게 됩니다.
이제 이 속성들의 의미를 하나하나 알아보겠습니다.
▷AddRemoveProgramIcon : 제어판 -> 프로그램 추가/제거에 표시될 아이콘을 등록합니다.
▷Author : 프로젝트 작성자의 이름을 등록합니다.
▷Desciption : 설치 관리자에 관한 설명을 등록합니다.
▷DetectNewerInstalledVersion : 프로그램 설치시 버전 비교를 통해 새 버전인지 확인 해 줍니다. 이미 설치 되있을 경우 설치 되어있다고 알려주기도 합니다.
▷Keyword : 설치 관리자를 검색하는데 사용 할 키워드를 지정합니다.
▷Localization : 로케일을 적용한다고 하는데, 글로벌 프로그램이 아니라면 신경 안써도 될 듯 합니다.
▷Manufacturer : 제조업체의 이름을 지정합니다
▷MunufacturerUrl : 제조업체의 홈페이지 링크를 지정.
▷PostBuildEvent : 배포프로젝트를 빌드한 후에 실행 할 명령줄을 지정합니다.
▷PreBuildEvent : 배포프로젝트를 빌드하기 전에 실행 할 명령줄을 지정합니다.
▷ProductCode : 응용프로그램의 고유 식별자(GUID)를 지정합니다.
▷ProductName : 프로그램의 공개 이름을 지정합니다.
▷RemovePrevionsVersions : 설치시 이전버전을 삭제 할지를 지정합니다.
▷RunPostBuildEvent : PostBuildEvent 속성에서 지정된 명령줄을 실행할 시기를 결정합니다.
▷SearchPath : 개발 컴퓨터의 어셈블리, 파일 또는 병합 모듈을 검색하는 데 사용되는 경로를 지정합니다.
Subject : 프로그램을 설명하는 추가정보를 지정합니다.
▷SupportPhone : 전화번호를 지정합니다.
▷SupportUrl : 마찬가지로 추가 설명을 하는 웹사이트 주소
TargetPlatform : 프로그램이 실행될 플랫폼을 지정.
▷Title : 설치 관리자의 제목을 지정합니다.
▷UpgradeCode : 프로그램 버전이 여러가지 일때 고유식별자를 지정합니다.
▷Virsion : 버전을 지정합니다.




속성 참 많네요.. 정작 중요한건 ProductName, Title, Author, MAnufacturer 정도 입니다.

이제 파일시스템 탭으로 이동하여 대상 컴퓨터의 파일 시스템 -> 응용 프로그램 폴더를 선택한 후 속성창을 확인합니다.

DefalutLocation이라는 속성에 [ProgramFilesFolder][Manufacturer]\[ProductName] 라고 되어 있습니다. 인스톨시 프로그램이 설치될 폴더 인데요. 이 설정으로 하게 되면 프로그램파일 폴더 밑에 제조회사명 밑에 프로그램 이름 폴더 안에 깔리게 되겠습니다. 맘에 드는 폴더로 변경 하시면 됩니다.

이제 파일을 추가 해 보겠습니다. 응용 프로그램 폴더에서 마우스 오른쪽 버튼을 누르면 파일이나 폴더등을 추가 할 수 있습니다. 미리 만들어 놓은 샘플 어플리케이션을 추가 하겠습니다.


이렇게 하면 앞서 지정된 설치 폴더에 MainApp.exe가 설치 됩니다. 명색이 인스톨 프로그램인데 이것만 지정하면 안되겠죠? 위에 사용자 바탕화면사용자 프로그램 메뉴가 보입니다.
말 그래로 사용자 바탕화면은 바탕화면에 설치 될 파일을 지정 할 수 있고, 사용자 프로그램 메뉴는 [시작]->[프로그램]의 폴더나 파일을 지정 할 수 있습니다.
바탕화면과 프로그램 메뉴에 MainApp.exe의 바로가기를 넣어주면 클라이언트가 아주 편리 할 것 같습니다.
MainApp.exe를 마우스 오른쪽 버튼으로 클릭하여 바로가기를 만듭니다. 저는 두개를 만들어 이름을 원하는데로 변경한 후 하나는 사용자 바탕화면으로 끌어다 놓았고, 사용자 프로그램 메뉴에는 [새폴더]를 하나 추가 하여, 그 안에 넣었습니다. 그리고 아이콘 파일(.ico)도 하나 추가하여 바로가기의 속성중 Icon에 연결 시켜 줍니다. 그럼 바로가기가 우리가 지정한 아이콘으로 생성 됩니다.

이제 언인스톨 기능을 하는 바로가기도 지원해 주어야 좀 더 있어보일 것입니다.
바탕화면에서 텍스트문서(txt)를 하나 추가해서 확장자를 bat로 바꾸어 줍니다. 편집기로 파일을 열어 Msiexec /x {ProductCode} 를 추가 해 줍니다. 여기서 ProductCode는 위에 프로젝트 속성중에 있던 코드 입니다. 지금 예제 대로 하면 Msiexec /x {A1715BBB-A953-4F01-B788-168542ED2BC3} 이 되겠네요.

현재 배포하는 샘플 프로그램이 매우 간단하여 파일이 하나이지만, 대부분의 응용은 여러 DLL을 포함하고 있을것입니다. 그리고 각 파일마다 설치하고 싶은 경로가 다를 수 있는데, 파일시스템 탭의 대상 컴퓨터의 파일 시스템을 오른쪽 버튼으로 누르면 특수폴더 추가에서 원하는 폴더를 추가 할 수 있습니다.

이제 이 파일을 응용프로그램 폴더에 포함 시켜주고, 마찬가지 방법으로 바로 가기를 만들어서 원하는 곳에 추가 시켜 줍니다. 프로젝트를 다시 빌드 하고 테스트 해보겠습니다. 프로젝트 폴더의 Relese 폴더에 들어가니 파일이 두개 보입니다.(Relese모드로 빌드 했을 경우, Debug 모드 일경우 Debug 폴더에 생성)
Setup.exe를 더블 클릭하니 설치가 잘 됩니다.
설치시 생긴 바로가기 아이콘으로 Uninstall도 잘되는지 확인해 보겠습니다.
정상적으로 잘 되는걸 볼 수 있습니다. 쓰다 보니 스크롤의 압박이군요;
만약 설치 대상컴퓨터(클라이언트)에 .Net Framwork가 없을때 자동으로 설치되게 하는 것 까지 쓰려고 했는데,
다음편으로 넘겨야 할 것 같습니다. 다음편에는 이같은 기능을 해주는 Boot Strapper에 관해 포스팅 하겠습니다.
Posted by 맨날맑음
,
안녕하세요? 맨날맑음 입니다.

WPF나 Silverlight 와는 다르게 Windows Form에는 '로드 완료' 이벤트가 없습니다. Load 이벤트가 제공 되고 있기는 하지만 MSDN에 나온 의미는 '폼이 처음으로 표시되기 전에 발생합니다.' 입니다. 즉 로드 완료가 아니라는 것입니다.

이것이 왜 문제일까?
로드 완료가 아닌 로드중 이벤트만 제공하여 생기는 문제가 있습니다.
예를 들어 프로그램을 시작할 때 폼을 숨기려고 한다고 하면, Hide() 메서드를 호출 해야합니다.
실행 할때 숨기려 했으니 당연히 폼의 생성자에 Hide()를 넣어보지만 전혀 동작 하지 않습니다.
두번째 방법으로 폼의 Load 이벤트 핸들러 안에 Hide()를 넣어봅니다. 역시 동작하지 않네요;
정확하지 는 않지만, MSDN의 설명대로 폼이 아직 표시되기 전이라 Hide()가 먹히지 않는 것 같습니다.
이밖에 Focus()나 몇몇 다른 메서드도 동작하지 않습니다.

어떻게 해결할까?
위의 문제를 해결하려면 '로드완료' 이벤트를 찾아야 하는데 아무리 뒤져보아도 로드완료는 존재하지 않았습니다.
그래서 임시 방편으로 Application.Idle 이벤트를 사용하여 해결 하였습니다. Application.Idle 이벤트는 말그대로 프로그램이 아이들 상태(쉬고 있을때) 발생합니다. 이걸 응용하면, 생성자에서 이벤트를 걸어주고, 이벤트 핸들러에서 이벤트를 빼주는 방법으로 로드 완료된 시점을 잡을 수 있습니다. 물론 엄밀히 말하면 프로그램이 더이상 할 일이 없어 쉬고 있을 시점을 잡아내는 것입니다.

using System;

using System.Windows.Forms;

namespace Sample

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

            //Idle이벤트를 걸어준다.

            Application.Idle += Application_Idle;

        }

        void Application_Idle(object sender, EventArgs e)

        {

            //Idle이벤트를 없앤다.

            Application.Idle -= Application_Idle;

            this.Hide();

        }

    }

}


위의 코드에서 Idle 이벤트 핸들러에서 Application.Idle -= Application_Idle; 이벤트를 빼주는 방법으로 프로그램이 처음으로 쉴때(로드완료)를 잡아내는 것입니다. 이렇게 임시 방편이나마 로드 완료 시점을 잡아서 hide() 메서드를 동작시킬 수 있습니다.
Posted by 맨날맑음
,