ListView 혹은 ListBox를 이용한 섬네일 만드는 클래스 입니다.

class ThumbnailView : ListView

class ThumbnailBox : ListBox

상속을 통해 만들었고 필요한 함수는 커스텀해서 사용 가능합니다.

 

ListView는 가로로도 확장이 가능하고,

ListBox는 세로만 가능하게 만들었고, 사이즈가 가변적으로 변경됩니다.

 

먼저 결과 및 사용 방법.

ListView, ListBox 베이스 섬네일

 

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        //폴더에 있는 이미지 파일 불러오기
        private void button_add_Click(object sender, EventArgs e)
        {
            string folder = @"C:\Test"; //폴더 경로
            string[] files = Directory.GetFiles(folder);

            button_add.Enabled = false; //추가 버튼

            Task.Run(() =>
            {
                int viewCount = thumbnailView1.GetCount(); //View Item 개수
                int boxCount = thumbnailBox1.GetCount(); //Box Item 개수

                foreach (string file in files)
                {
                    thumbnailView1.AddThumbnail(file, viewCount++.ToString("D4")); //View에 섬네일 추가
                    thumbnailBox1.AddThumbnail(file, boxCount++.ToString("D4")); //Box에 섬네일 추가
                }

                Invoke((MethodInvoker)delegate
                {
                    button_add.Enabled = true; //추가 버튼
                });
            });
            
            //GetSelectedPathList, RemoveSelectedThumbnail, RemoveAllThumbnail 있음.
        }
    }
}

 

ListView 클래스 (빌드 후 사용)

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WindowsFormsApp1 //네임스페이스 맞춰줄 것.
{
    public class ThumbnailView : ListView
    {
        [DllImport("user32.dll")]
        private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

        private int thumbnailWidth = 128;
        private int thumbnailHeight = 128;

        public ThumbnailView()
        {
            this.View = View.LargeIcon;
            this.MultiSelect = true; //여러 장 선택
            this.OwnerDraw = false; //기본 그리기 사용
            this.HideSelection = false; //포커싱 잃으면 선택 취소

            ImageList imageList = new ImageList();
            imageList.ColorDepth = ColorDepth.Depth32Bit;
            imageList.ImageSize = new Size(thumbnailWidth, thumbnailHeight); // 원하는 썸네일 크기
            this.LargeImageList = imageList;
        }

        protected override void OnHandleCreated(EventArgs e) //깜박임 방지
        {
            base.OnHandleCreated(e);
            SendMessage(this.Handle, 0x1000 + 54, 0x00010000, 0x00010000); // LVS_EX_DOUBLEBUFFER
        }

        public int GetCount() //현재 개수 구하기
        {
            return this.Items.Count;
        }

        public void AddThumbnail(string path, string name) //섬네일 추가
        {
            using (Image original = Image.FromFile(path))
            {
                Image thumbnail = MakeThumbnail(original, thumbnailWidth, thumbnailHeight);
                int index = this.LargeImageList.Images.Count;

                InvokeAuto(() =>
                {
                    this.LargeImageList.Images.Add(thumbnail);
                    ListViewItem item = new ListViewItem();
                    item.Text = name; //섬네일 이름
                    item.ImageIndex = index; //새로운 인덱스
                    item.Tag = path; // 원본 경로 저장 (object형태 tag에 저장)
                    this.Items.Add(item);
                });
            }
        }

        private Image MakeThumbnail(Image image, int width, int height) //섬네일 생성
        {
            Bitmap bitmap = new Bitmap(width, height);
            using (Graphics g = Graphics.FromImage(bitmap))
            {
                g.Clear(Color.White);
                g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;

                //이미지 비율 유지
                double ratio = Math.Min((double)width / image.Width, (double)height / image.Height);
                int newWidth = (int)(image.Width * ratio);
                int newHeight = (int)(image.Height * ratio);

                int x = (width - newWidth) / 2;
                int y = (height - newHeight) / 2;

                g.DrawImage(image, x, y, newWidth, newHeight); //섬네일 그리기
                g.DrawRectangle(Pens.Gray, 0, 0, width - 1, height - 1); //테두리 그리기
            }

            return bitmap;
        }

        private void InvokeAuto (Action action) //스레드 사용 대비
        {
            if (InvokeRequired)
            {
                Invoke((MethodInvoker)delegate
                {
                    action();
                });
            }
            else
            {
                action();
            }
        }

        public List<string> GetSelectedPathList() //선택 된 경로 반환
        {
            if (this.SelectedIndices == null)
            {
                return new List<string>();
            }

            List<string> pathList = new List<string>();

            foreach (int index in this.SelectedIndices)
            {
                pathList.Add(this.Items[index].Tag as string);
            }

            return pathList;
        }

        public void RemoveSelectedThumbnail() //선택 된 요소 삭제
        {
            if (this.SelectedIndices == null || this.SelectedIndices.Count <= 0)
            {
                return;
            }

            this.BeginUpdate();

            //뒷쪽부터 삭제
            for (int i = this.SelectedIndices.Count - 1; i >= 0; --i)
            {
                int index = this.SelectedIndices[i];
                Remove(index);
            }

            this.EndUpdate();
        }

        public void RemoveAllThumbnail() //모든 요소 삭제
        {
            if (this.Items.Count <= 0)
            {
                return;
            }

            this.BeginUpdate();

            for (int i = this.Items.Count - 1; i >= 0; --i)
            {
                Remove(i);
            }

            this.EndUpdate();
        }

        private void Remove(int index)
        {
            this.Items.RemoveAt(index);

            this.LargeImageList.Images[index].Dispose();
            this.LargeImageList.Images.RemoveAt(index);
        }
    }
}

 

 

ListBox 클래스

 

※ ItemHeight가 변경 되면 공백이 남는 경우가 생깁니다.

이에따라 고정크기 panel 하위에 listbox를 생성하고, 현재 보여지는 크기에 맞추어 ItemHeight가 가변적으로 변경됩니다.

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.Generic;

namespace WindowsFormsApp1
{
    class ThumbnailBox : Panel
    {
        private class ThumbnailInfo
        {
            public Image NormalImage { get; set; }
            public Image FocusImage { get; set; }
            public string Path { get; set; }
            public string Name { get; set; }
        }

        private readonly Color highlighColor = Color.FromArgb(80, 0, 120, 215);

        private readonly int thumbnailMaxSize = 128; //최대 사이즈 고정 (255넘으면 섬네일 생성 실패)
        private readonly int thumbnailSize = 128;

        private readonly int imageFixedHeight = 110;
        private readonly int textHeight = 30;
        private readonly int margin = 3;

        private List<ThumbnailInfo> thumbnailInfoList = new List<ThumbnailInfo>();
        private ListBox listBox = new ListBox();

        public ThumbnailBox()
        {
            //리스트박스 사이즈가 가변일 수 있어 panel하위에서 크기 체크.
            this.Controls.Add(listBox);
            this.SizeChanged += panel_SizeChanged;
            this.BackColor = Color.White;
            this.BorderStyle = BorderStyle.FixedSingle;
            this.Margin = new Padding(0);

            listBox.Margin = new Padding(0);
            listBox.BackColor = Color.Gray;
            listBox.Dock = DockStyle.Fill;
            listBox.DrawMode = DrawMode.OwnerDrawFixed; //직접 그려주기
            listBox.DrawItem += ThumbnailBox_DrawItem; //직접 그려주기
            listBox.ItemHeight = imageFixedHeight; //아이템 높이 설정
            listBox.BorderStyle = BorderStyle.None;
            listBox.SelectionMode = SelectionMode.MultiExtended; //다중 선택
        }

        private void panel_SizeChanged(object sender, EventArgs e)
        {
            int panelHeight = this.ClientSize.Height; //실제 표시 가능한 영역

            int visibleCount = (int)Math.Round((double)panelHeight / imageFixedHeight); //패널 높이에 따라 표시할 아이템 개수 결정
            visibleCount = Math.Max(visibleCount, 1); //최소 1개 보장

            listBox.ItemHeight = panelHeight / visibleCount; //꽉 차게 분배
            listBox.Invalidate();
        }

        public int GetCount() //현재 개수 구하기
        {
            return listBox.Items.Count;
        }

        public void AddThumbnail(string path, string name) //섬네일 추가
        {
            try
            {
                ThumbnailInfo thumbnailInfo = new ThumbnailInfo
                {
                    Path = path,
                    Name = name,
                };

                using (Image image = Image.FromFile(path))
                {
                    int width = image.Width;
                    int height = image.Height;

                    double ratio = (double)Math.Min(thumbnailSize, thumbnailMaxSize) / (double)Math.Max(width, height);

                    int newWidth = (int)(width * ratio);
                    int newHeight = (int)(height * ratio);

                    //섬네일 생성
                    Image thumbnail = image.GetThumbnailImage(newWidth, newHeight, null, IntPtr.Zero);
                    thumbnailInfo.NormalImage = thumbnail;

                    //선택 시 하이라이트 섬네일 생성
                    Bitmap bitmap = new Bitmap(thumbnail.Width, thumbnail.Height);
                    using (Graphics g = Graphics.FromImage(bitmap))
                    {
                        g.DrawImage(thumbnail, 0, 0, bitmap.Width, bitmap.Height);

                        using (Brush brush = new SolidBrush(highlighColor))
                        {
                            g.FillRectangle(brush, 0, 0, bitmap.Width, bitmap.Height);
                        }
                    }
                    thumbnailInfo.FocusImage = bitmap;
                }

                thumbnailInfoList.Add(thumbnailInfo);

                //이름으로 아이템 추가
                if (InvokeRequired)
                {
                    Invoke((MethodInvoker)delegate
                    {
                        listBox.Items.Add(name);
                    });
                }
                else
                {
                    listBox.Items.Add(name);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }

        private void ThumbnailBox_DrawItem(object sender, DrawItemEventArgs e)
        {
            e.DrawBackground();

            int index = e.Index;
            if (index < 0)
            {
                return;
            }

            //일반 or 선택 시 백그라운드 및 이미지
            Brush background = Brushes.White;
            Image image = thumbnailInfoList[index].NormalImage;
            int itemHeight = listBox.ItemHeight;

            if ((e.State & DrawItemState.Selected) == DrawItemState.Selected) //선택 된 상태
            {
                background = Brushes.DodgerBlue;
                image = thumbnailInfoList[index].FocusImage;
            }

            //배경 그리기
            e.Graphics.FillRectangle(background, e.Bounds.X + margin, e.Bounds.Y + margin, e.Bounds.Width - margin * 2, e.Bounds.Height - margin);

            //이미지 그리기
            e.Graphics.DrawImage(image, e.Bounds.Left + margin, e.Bounds.Top + margin, e.Bounds.Width - margin * 2, itemHeight - textHeight);

            //텍스트 그리기
            using (StringFormat stringFormat = new StringFormat() { Alignment = StringAlignment.Center }) //텍스트 가운데 정렬
            {
                e.Graphics.DrawString(thumbnailInfoList[index].Name,
                                      e.Font, //폰트
                                      Brushes.Black, //색상
                                      (e.Bounds.Left + e.Bounds.Right) / 2, //위치x
                                      e.Bounds.Top + itemHeight - textHeight + 10, //위치y
                                      stringFormat);
            }

            //선택 시 테두리
            e.DrawFocusRectangle();
        }

        public List<string> GetSelectedPathList() //선택 된 경로 반환
        {
            if (listBox.SelectedIndices == null)
            {
                return new List<string>();
            }

            List<string> pathList = new List<string>();

            foreach (int index in listBox.SelectedIndices)
            {
                pathList.Add(thumbnailInfoList[index].Path);
            }

            return pathList;
        }

        public void RemoveSelectedThumbnail() //선택 된 요소 삭제
        {
            if (listBox.SelectedIndices == null || listBox.SelectedIndices.Count <= 0)
            {
                return;
            }

            listBox.BeginUpdate();
            //뒷쪽부터 삭제
            for (int i = listBox.SelectedIndices.Count - 1; i >= 0; --i)
            {
                int index = listBox.SelectedIndices[i];
                Remove(index);
            }

            listBox.EndUpdate();
            listBox.Refresh();
        }

        public void RemoveAllThumbnail() //모든 요소 삭제
        {
            if (listBox.Items.Count <= 0)
            {
                return;
            }

            listBox.BeginUpdate();

            //뒷쪽부터 삭제
            for (int i = listBox.Items.Count - 1; i >= 0; --i)
            {
                Remove(i);
            }

            listBox.EndUpdate();
            listBox.Refresh();
        }

        private void Remove(int index)
        {
            listBox.Items.RemoveAt(index);

            //이미지 리소스 해제 필요
            thumbnailInfoList[index].NormalImage.Dispose();
            thumbnailInfoList[index].FocusImage.Dispose();
            thumbnailInfoList.RemoveAt(index);
        }
    }
}

C# 에서 기본적으로 제공되는 압축 해제 기능으로

System.IO.Compression 가 있습니다.

암호화는 지원하지 않지만 기본 내장되어 있고 사용도 편리합니다.

 

ZipFile 클래스 사용을 위해선 System.IO.Compression.FileSystem 참조가 필요합니다.

 

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression; //ZipFile 에 필요
using System.Windows.Forms;

namespace WindowsFormsApp13
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e) //zip 생성
        {
            string workFolder = Application.StartupPath;
            string targetDirectory = Path.Combine(workFolder, "TestFiles");
            string savePath = Path.Combine(workFolder, "Zipfile.zip");

            if (File.Exists(savePath)) //기존 파일은 제거
            {
                File.Delete(savePath);
            }

            ZipFile.CreateFromDirectory(targetDirectory, savePath);
            Process.Start(workFolder); //폴더 열어서 확인
        }

        private void button2_Click(object sender, EventArgs e) //zip 해제
        {
            string workFolder = Application.StartupPath;
            string targetPath = Path.Combine(workFolder, "Zipfile.zip");
            string saveFolder = Path.Combine(workFolder, "unZip");

            if (Directory.Exists(saveFolder)) //기존 파일은 제거
            {
                Directory.Delete(saveFolder, true);
            }
            
            ZipFile.ExtractToDirectory(targetPath, saveFolder);
            Process.Start(workFolder); //폴더 열어서 확인
        }
    }
}

 

 

파일 압축 및 풀기 결과

 

그 밖에도 다른 라이브러리들이 있는데, NuGet 에서 받아서 사용이 가능합니다.

GPT에서 제공한 표를 남겨두겠습니다.

 

- GPT 정리 내용

라이브 러리 암호화 포맷 속도 최신 유지장점 요약 (2025년 10월 06일 기준!)

라이브러리 암호화 포맷 속도 최신 유지 장점 요약
System.IO.Compression ZIP ✅ 빠름 ✅ 최신 .NET 내장, 간단
DotNetZip (Ionic.Zip) ZIP ⚪ 보통 ❌ 구버전 암호 ZIP 지원
SharpZipLib ZIP, TAR, GZIP 등 ⚪ 보통 ✅ 유지보수 중 다양한 포맷
SharpCompress ⚠️ 제한적 ZIP, 7z, TAR 등 ⚪ 보통 ✅ 최신 스트리밍 지원
SevenZipSharp ✅ AES 7z ⚠️ 느림 ⚪ 부분적 고압축, 보안

 

DotNetZip 간단 사용법 예시

using Ionic.Zip;

// 압축
using (var zip = new ZipFile())
{
    zip.Password = "1234"; // 암호 설정
    zip.AddDirectory(@"C:\TestSource");
    zip.Save(@"C:\Result.zip");
}

// 해제
using (var zip = ZipFile.Read(@"C:\Result.zip"))
{
    zip.Password = "1234";
    zip.ExtractAll(@"C:\Extracted", ExtractExistingFileAction.OverwriteSilently);
}

DateTime 으로 시간을 확인 할 수 있지만, Stopwatch 를 통해 경과 시간을 확인 할 수 있습니다.

내부 요소를 통해 아래의 방법대로 표현도 가능합니다.

 

 

using System;
using System.Diagnostics;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch stopWatch = Stopwatch.StartNew(); //객체 만들자마자 Start()까지 실행됨
            
            while (stopWatch.ElapsedMilliseconds < 5000) //5초
            {
                try
                {
                    using (TcpClient tcpClient = new TcpClient()) //연결만 확인.
                    {
                        Task task = tcpClient.ConnectAsync("127.0.0.1", 8080); //연결할 주소
                        if (task.Wait(3000) && tcpClient.Connected) //연결 확인
                        {
                            tcpClient.Close();
                            break;
                        }
                    }
                }
                catch (Exception ex)
                {
                    //Console.WriteLine(ex); //서버 없으면 오류나서 주석처리
                }

                Console.WriteLine($"tick: {stopWatch.ElapsedTicks}");
                Console.WriteLine($"milliseconds: {stopWatch.ElapsedMilliseconds}");

                TimeSpan timeSpan = stopWatch.Elapsed;
                Console.WriteLine($"기본: {timeSpan}");
                Console.WriteLine($"초: {timeSpan.TotalSeconds}"); //전체 초 단위
                Console.WriteLine($"ms: {timeSpan.TotalMilliseconds}"); //전체 밀리초 단위
                Console.WriteLine($"포맷: {timeSpan:hh\\:mm\\:ss\\.fff}"); //00:00:00.000

                Console.WriteLine();
            }

            Console.ReadLine();
        }
    }
}

오랜만에 기본기에 대해 생각을 해보게 되었습니다.

값 복사 형식과 참조 형식.. (잊고 있던 포인터..)

코드에 대부분 클래스를 사용하다보니 잊고 있었는데 복습할 겸 글을 남겨봅니다.

가독성을 위해 foreach 를 사용하려고 하는데 구조체에 대해서는 메모리 낭비가 발생 되고,

foreach 내부에서 원본의 값을 수정할 수 없다는 단점이 있더군요..!

추가로 배열 사이즈 변경도 남겨봅니다.

 

 

using System;
using System.Collections.Generic;

namespace ConsoleApp2
{
    class Program
    {
        struct ValueType
        {
            public int a;
            public int b;
        }

        class ReferenceType
        {
            public int a;
            public int b;
        }

        static void Main(string[] args)
        {
            ValueType valueType; //new ValueType() 없이 사용 가능.
            valueType.a = 1;
            valueType.b = 2;

            ValueType valueType2 = valueType; //값 복사됨.
            valueType2.a = 3;
            valueType2.b = 4;

            ReferenceType referenceType = new ReferenceType(); //반드시 메모리 할당 필요
            referenceType.a = 1;
            referenceType.b = 2;

            ReferenceType referenceType2 = referenceType; //같은 메모리 바라봄
            referenceType2.a = 3;
            referenceType2.b = 4;

            Console.WriteLine("valueType : {0} {1}", valueType.a, valueType.b);
            Console.WriteLine("valueType2 : {0} {1}", valueType2.a, valueType2.b);

            Console.WriteLine("referenceType : {0} {1}", referenceType.a, referenceType.b);
            Console.WriteLine("referenceType2 : {0} {1}", referenceType2.a, referenceType2.b);

            List<ValueType> valueTypeList = new List<ValueType>();
            List<ReferenceType> referenceTypeList = new List<ReferenceType>();

            //ValueType은 foreach에서 값 변경이 불가능하다, 또한 메모리 낭비가 발생 될 수 있다.
            foreach (ValueType sub in valueTypeList)
            {
                Console.WriteLine("{0} {1}", sub.a, sub.b);
                //sub.a = 40; //구조체는 오류 발생. 복사 값이므로 원본을 바꿀 수 없어 컴파일 오류.
            }

            foreach (ReferenceType sub in referenceTypeList) //foreach 도 참조형태 - 원본 값 바뀜
            {
                Console.WriteLine("{0} {1}", sub.a, sub.b);
                sub.a = 40; //원본 값 변경 됨.
            }

            ValueType[] valueTypeArray = null;
            Array.Resize(ref valueTypeArray, 5); //new ValueType[5]

            for (int i = 0; i < valueTypeArray.Length; ++i)
            {
                Console.WriteLine("배열 개수 : {0}", i + 1);
            }

            Console.ReadLine();
        }
    }
}

A프로그램에서 B프로그램에게 요청하여 동작을 할 일이 있었습니다.

B프로그램은 초기화 되며 열려있는 창들이 닫혀야 했는데 이 부분에 대한 구현이 필요했습니다.

 

ex)

Program (B) 기능 -> OpenFileDialog (윈도우 API) Custom Form (사용자Form)

 

Program (B) 가 ShowDialog 로 잡혀있는 상황입니다.

윈도우API 종료는 SendMessage 를 사용해야 하고, Form은 Close를 통해 종료해 줄 수 있습니다.

 

(NamedPipe를 사용하였고, BlockingCollection Queue 를 사용하여 순서유지를 보장하였습니다.

코드가 좀 길어졌지만.. 남겨봅니다!

 

ShowDialog() 종료하는 프로그램

 

A프로그램 (명령 내리는 코드)

using System;
using System.IO;
using System.IO.Pipes;
using System.Windows.Forms;

namespace WindowsFormsApp6
{
    public partial class Form2 : Form
    {
        private const string targetName = "A_Pipe";

        public Form2()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            SendNamedPipe("CloseAPI");
        }

        private void button2_Click(object sender, EventArgs e)
        {
            SendNamedPipe("CloseSubForm");
        }

        private void SendNamedPipe(string message)
        {
            try
            {
                NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", targetName, PipeDirection.InOut);
                namedPipeClientStream.Connect(10000); //10초

                StreamWriter streamWriter = new StreamWriter(namedPipeClientStream);

                streamWriter.WriteLine(message);
                streamWriter.Flush();

                streamWriter.Close();
                namedPipeClientStream.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }
}

 

 

B프로그램 (ShowDialog 하는 프로그램)

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form3 : Form
    {
        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        private const uint WM_CLOSE = 0x0010;
        private const string myName = "A_Pipe"; //이름만 바꿔서 사용 가능

        private BlockingCollection<string> queue = new BlockingCollection<string>();
        private int queueCount = 0;

        private void ProcessQueue()
        {
            //내부적으로 monitor lock 걸려있어 순서 및 단일 실행 보장
            foreach (string message in queue.GetConsumingEnumerable())
            {
                if (message == "CloseAPI")
                {
                    CloseAPI();
                }
                else if (message == "CloseSubForm")
                {
                    CloseSubForm();
                }

                Invoke((MethodInvoker)delegate
                {
                    queueCount++;
                    listBox1.Items.Add(string.Format("{0}_{1}", queueCount, message));
                });
            }
        }

        public Form3()
        {
            InitializeComponent();

            //큐에서 관리
            Task.Run(() =>
            {
                ProcessQueue();
            });

            //네임드파이프 통신
            Task.Run(() =>
            {
                Receive();
            });
        }

        private void Receive()
        {
            while (true)
            {
                try
                {
                    NamedPipeServerStream namedPipeServerStream = new NamedPipeServerStream(myName, PipeDirection.InOut, 10);
                    namedPipeServerStream.WaitForConnection(); //데이터 받을때까지 기다립니다.

                    StreamReader streamReader = new StreamReader(namedPipeServerStream);
                    string message = streamReader.ReadLine();

                    queue.Add(message); //큐에 데이터 쌓아주기

                    streamReader.Close();
                    namedPipeServerStream.Close();
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();
            ofd.Title = "불러오기 테스트";

            //테스트하기 위해 띄워주기만 함.
            if (ofd.ShowDialog() == DialogResult.OK)
            {
                //정상 OK 눌렀을 때 실행
            }
            else
            {
                //강제 종료시 이 곳을 실행.
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            //테스트용으로 단순 띄워주기만 진행
            new Form1().ShowDialog();
        }

        private void CloseAPI()
        {
            IntPtr hWnd = FindWindow(null, "불러오기 테스트"); //타이틀명으로 찾기
            if (hWnd != IntPtr.Zero)
            {
                SendMessage(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); //종료 메시지 보내기
            }
        }

        private void CloseSubForm()
        {
            List<Form> formList = new List<Form>();

            foreach (Form form in Application.OpenForms)
            {
                // 메인폼 제외
                if (form is Form3 == false)
                {
                    formList.Add(form);
                }
            }

            foreach (Form form in formList)
            {
                Invoke((MethodInvoker)delegate
                {
                    form.Close();
                });
            }
        }
    }
}

파일 등을 드래그 & 드롭으로 가져올 수 있는 코드입니다.

dropFiles 는 해당 파일의 경로를 불러오게 됩니다.

 

파일/폴더를 마우스 드래그로 가져올 수 있습니다.

 

using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            listBox1.AllowDrop = true; //drag & drop 허용
        }

        private void listBox1_DragDrop(object sender, DragEventArgs e)
        {
            //드래그 드롭 파일 (여러 개 가능)
            string[] dropFiles = (string[])e.Data.GetData(DataFormats.FileDrop);
            
            foreach (string file in dropFiles)
            {
                listBox1.Items.Add(file);
            }
        }

        private void listBox1_DragEnter(object sender, DragEventArgs e)
        {
            //마우스 커서 표현
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
            }
        }
    }
}

종료 되었지만 남아있는 tray 아이콘

 

C# 에서 정상적으로 프로그램이 종료되면 Tray 아이콘이 제거 되긴 합니다만,

프로세스 킬, 비정상적인 종료 방법이라면 위와 같이 트레이 아이콘이 남는 경우가 있습니다.

 

해당 아이콘은 mouse hover 이벤트 시 윈도우 내부적으로 해당 프로그램이 동작하고 있는지 판별하기에

mouse move 이벤트를 통해 해당 tray들을 정리할 수 있습니다. 아래는 코드!

 

[생성, 정리 후]

생성 버튼 클릭 (8번)
정리 버튼 클릭

 

 

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        class TrayRefresher
        {
            [StructLayout(LayoutKind.Sequential)]
            struct RECT
            {
                public int Left;
                public int Top;
                public int Right;
                public int Bottom;
            }

            [DllImport("user32.dll")]
            static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr childAfter, string className, string windowName);

            [DllImport("user32.dll")]
            static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);

            [DllImport("user32.dll")]
            static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

            const int WM_MOUSEMOVE = 0x0200;
            
            public void RefreshTray()
            {
                /*
                Shell_TrayWnd (하단의 작업 표시줄)
                 ├─ TrayNotifyWnd (시스템 트레이 영역 - 시계, 네트워크 아이콘, 볼륨 조절 등)
                 │    └─ SysPager (아이콘을 스크롤하거나 관리)
                 │         └─ ToolbarWindow32 (보이는 트레이 아이콘들)
                 │
                 └─ NotifyIconOverflowWindow (숨겨진 아이콘 영역) - 숨기기/펼치기 영역
                      └─ ToolbarWindow32 (숨겨진 아이콘들)
                */

                //보여지는 영역
                IntPtr toolbar = FindToolbar("Shell_TrayWnd", "TrayNotifyWnd", "SysPager", "ToolbarWindow32");
                SendMouseMove(toolbar);

                //감춰진 영역도 추가
                toolbar = FindToolbar("NotifyIconOverflowWindow", "ToolbarWindow32");
                SendMouseMove(toolbar);
            }

            private IntPtr FindToolbar(params string[] targets)
            {
                IntPtr hWnd = IntPtr.Zero;

                foreach (string target in targets)
                {
                    hWnd = FindWindowEx(hWnd, IntPtr.Zero, target, null);

                    if (hWnd == null)
                    {
                        break;
                    }
                }

                return hWnd;
            }

            private void SendMouseMove(IntPtr hWnd)
            {
                if (hWnd == null)
                {
                    return;
                }

                if (GetWindowRect(hWnd, out RECT rect))
                {
                    int width = rect.Right - rect.Left;
                    int height = rect.Bottom - rect.Top;
                    int offset = 5; //아이콘 간격

                    for (int x = 0; x < width; x += offset)
                    {
                        for (int y = 0; y < height; y += offset)
                        {
                            //4byte lParam에 2byte씩 마우스 위치를 넣어줍니다.
                            //mouse y 2byte, mouse x 2byte
                            IntPtr lParam = (IntPtr)((y << 16) | (x & 0xFFFF));
                            SendMessage(hWnd, WM_MOUSEMOVE, IntPtr.Zero, lParam);
                        }
                    }
                }
            }
        }

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Task.Run(async () => //테스트용 트레이 아이콘 생성!
            {
                //테스트용 프로그램            
                Process process = Process.Start(@"...\trayTest.exe");
                await Task.Delay(1000);
                process.Kill(); //강제 종료 시 트레이 아이콘이 남습니다.
            });
        }

        private void button2_Click(object sender, EventArgs e)
        {
            new TrayRefresher().RefreshTray(); //트레이 아이콘 정리!
        }
    }
}

크롬이나 엣지를 감지하여 따라다니는 프로그램 샘플입니다.

여러 개인 경우는 다르게 처리가 필요합니다 기능이 재미있어서 게시 해둬봅니다!

using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class FollowForm : Form
    {
        // EnumWindows 콜백 델리게이트
        private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

        [DllImport("user32.dll")]
        private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

        //SetLastError 설정하면 아래와 같이 에러 타입 가져올 수 있습니다.
        //int errorCode = Marshal.GetLastWin32Error();
        [DllImport("user32.dll", SetLastError = true)]
        private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool SetWindowPos(IntPtr hWnd, 
                                                IntPtr hWndInsertAfter,
                                                int X, 
                                                int Y, 
                                                int cx, 
                                                int cy, 
                                                uint uFlags);

        [DllImport("user32.dll")]
        private static extern bool IsWindowVisible(IntPtr hWnd);

        const uint SWP_NOMOVE = 0x0002;
        const uint SWP_NOSIZE = 0x0001;

        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }

        //EntryPoint를 통해 C#에서 사용할 이름을 커스텀 할 수 있습니다.
        [DllImport("user32.dll", EntryPoint = "GetWindowRect")]
        private static extern bool GetWindowRect22(IntPtr hWnd, out RECT lpRect);

        private bool isRunning = true;

        public FollowForm()
        {
            InitializeComponent();
        }

        private void FollowForm_Load(object sender, EventArgs e)
        {
            IntPtr myHwnd = this.Handle;

            Task.Run(async () =>
            {
                while (isRunning)
                {
                    await Task.Delay(10);

                    EnumWindows((hWnd, lParam) => //최상위 윈도우 값들을 가져옵니다.
                    {
                        if (IsWindowVisible(hWnd)) //윈도우 보여지는지 여부
                        {
                            StringBuilder title = new StringBuilder(256);
                            GetWindowText(hWnd, title, title.Capacity); //윈도우 텍스트를 가져옵니다.
                            string windowTitle = title.ToString();

                            //크롬이나 엣지를 감지합니다.
                            if (windowTitle.Contains("Chrome") || windowTitle.Contains("Edge"))
                            {
                                Console.WriteLine(windowTitle);

                                SetWindowPos(hWnd, myHwnd, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); //해당 위치를 조정해줍니다.

                                RECT rect;
                                GetWindowRect22(hWnd, out rect); //위치를 가져옵니다. GetWindowRect

                                Invoke((MethodInvoker)delegate
                                {
                                    this.Location = new Point(rect.Left, rect.Top);
                                });
                            }
                        }
                        return true;
                    }, IntPtr.Zero);
                }
            });
        }
        
        private void FollowForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            isRunning = false;
        }
    }
}

코드 및 디자이너는 아래와 같습니다.. 디자이너에서 트레이 아이콘 설정을 해주면 완료!

 

+) 추가적으로 보이지 않지만 특정 위치에 파일 다이얼로그, 프린트 등 기능을 보여주기 위해서

위치 값을 조정할 때가 있습니다. WindowState 와 Visible 처리를 통해 해당 위치 제어가 가능!

using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Tray : Form
    {
        public Tray()
        {
            InitializeComponent();

            this.ShowInTaskbar = false; //하단 테스트바에서 보이지 않도록 설정
            this.Visible = false; //화면에 보이지 않도록 설정
            this.WindowState = FormWindowState.Minimized; //최소화
            this.Opacity = 0; //투명하게 하여 타이틀도 안보이게 설정
            this.FormBorderStyle = FormBorderStyle.FixedToolWindow; //Alt + Tab 에서도 보이지 않도록

            //this.종료ToolStripMenuItem.Click += new System.EventHandler(this.종료ToolStripMenuItem_Click);

            //잠시 특정 위치 활성화
            this.WindowState = FormWindowState.Normal;
            this.Visible = true;
            //new OpenFileDialog().ShowDialog();

            //다시 특정 위치 해제
            this.WindowState = FormWindowState.Minimized;
            this.Visible = false;
        }

        private void 종료ToolStripMenuItem_Click(object sender, System.EventArgs e)
        {
            Close();
        }
    }
}

 

notifyIcon 및 contextMenuStrip 을 설정해줍니다.

notifyIcon 설정

 

ContextMenuStrip 설정 (이벤트도 추가 해주기)

 

결과 Tray Icon

 

.NET Framework / .NET 6 이상
[App.config] 파일

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <appSettings>
    <add key="ServerUrl" value="127.0.0.1"/>
    <add key="ServerPort" value="5000"/>
  </appSettings>
</configuration>

//코드에서 호출방법
System.Configuration.ConfigurationManager.AppSettings["ServerUrl"]
System.Configuration.ConfigurationManager.AppSettings["ServerPort"]

==============================================

.NET Core / .NET 5
[appsettings.json] 파일

{
  "ServerUrl": "https://example.com/api",
  "ServerPort": "5"
}

//코드에서 호출
var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();

string apiUrl = config["ServerUrl"];
string apiPort = config["ServerPort"];

+ Recent posts