윈도우 이동시키기

 

        private bool onClick;
        private Point startPoint = new Point(0, 0);
        private void moveWindow_MouseMove(object sender, MouseEventArgs e)
        {
            if(onClick)
            {
                Point p = PointToScreen(e.Location);
                Location = new Point(p.X - this.startPoint.X, p.Y - this.startPoint.Y);
            }
        }
        private void moveWindow_MouseDown(object sender, MouseEventArgs e)
        {
            onClick = true;
            startPoint = new Point(e.X, e.Y);
        }
        private void moveWindow_MouseUp(object sender, MouseEventArgs e)
        {
            onClick = false;
        }

 

판넬이나 기타 컨트롤을 마우스로 드래그해서 윈도우를 이동시키려면

아래처럼 해당 컨트롤에 이벤트 추가

 

 

윈도우 최소화시키기

        private void btnMinimize_Click(object sender, EventArgs e)
        {
            this.WindowState = FormWindowState.Minimized;
        }

해당 함수를 최소화 버튼의 클릭 이벤트에 등록

다른 쓰레드에서 폼 종료하려 할때 크로스 스레드 뭐라 뜨면서 안될때

델리게이트를 사용하면 됩니당

 

MainFormClose(); 호출하면 OK

        private delegate void MainFormCloseDelegate();
        private void MainFormClose()
        {
            if (InvokeRequired)
            {
                MainFormCloseDelegate del = new MainFormCloseDelegate(MainFormClose_threadSafe);
                this.Invoke(del);
            }
            else MainFormClose_threadSafe();
        }
        private void MainFormClose_threadSafe()
        {
            MessageBox.Show("프로그램 종료");
            this.Close();
        }

[리스트뷰 이름].Items.Add([아이템]);

[리스트뷰 이름].EnsureVisible([리스트뷰 이름].Items.Count - 1);

 

아래쪽에 추가되면 스크롤 자동으로 아래로 이동

서버 폼에서 /open 명령어로 서버를 엽니다.

 

서버가 열렸습니다.

 

 

클라이언트 폼입니다. 타 클라이언트의 접속 및 퇴장을 알 수 있습니다.

서버측 폼입니다. 클라이언트의 접속 및 퇴장을 알 수 있습니다.

 

 

+

채팅 화면은 위와 같습니다. 맨 위 클라이언트에서 Failed to Connect가 뜬 이유는 아이피 입력시 오타가 나서..

같은 공유기 내에 아이피로 진행했습니다. (데스크탑-서버, 노트북-클라이언트)

 

 

봐주셔서 감사합니다. 격한 훈수 환영합니다.

 

깃허브

https://github.com/hyomen/CSharp_WinForm_Chat

ID 입력창입니다. 

ID 입력 후 버튼 클릭 또는 키보드 엔터키 입력시 현재 창을 숨기고 클라이언트 폼을 띄웁니다.

 

클라이언트 폼입니다. 서버폼이랑 별 다를건 없습니다.

0. 전체 폼

FormClosing시 Disconnect() 메소드를 실행하게끔 합니다.

1. 채팅로그(RichTextBox, Name : ChatLog)

채팅 로그를 띄워줍니다. 서버 콘솔창과 마찬가지로 RichTextBox입니다.

2. 채팅 참가자 목록(ListBox, Name : ChatterList)

채팅 참가자 목록을 띄웁니다. 

3. 대화 입력창(TextBox, Name : InputField)

일반 TextBox입니다. 키보드 엔터키 입력시 마찬가지로 전송되게끔 합니다.

이를 위해 KeyDown시 InputField_KeyDown() 메소드를 실행케 합니다.

4. 전송 버튼(Button, Name : EnterBtn)

클릭시 EnterBtn_Click() 메소드를 실행합니다.

5. 나가기 버튼(Button, Name : QuitBtn) // 나가는 명령어가 /exit라 ExitBtn으로 통일하는걸 깜빡했습니다..,

서버와 접속을 끊는 Disconnect() 메소드를 실행합니다.

 

ClientForm.cs 코드입니다.

Public CilentForm(string ID)

유저ID를 ID입력 폼에서 받아 폼을 생성합니다.

void Connect(string s)

InputField에서 /connect [클라이언트IP] [서버IP] [서버PORT] 입력시 서버와 연결합니다.

' '기준으로 문자열을 split해 IP를 얻어냅니다.

연결 실패시 메세지박스를 띄우며 적당히 예외처리를 추가하시면 되겠습니다.

void Display(string msg)

ChatLog에 메세지를 띄웁니다. 

서버와 마찬가지로 CheckForIllegalCrossThreadCalls = false; 를 사용해 크로스 쓰레드 예외를 피합니다.

void Receiver()

서버로부터 오는 메세지를 받는 메소드입니다. 컨트롤 메세지일 경우 ChatLog에 띄우지 않고 따로 처리합니다.

bool isControlMsg(string msg)

컨트롤 메세지인 경우 그에 맞는 처리를 하고 ChatLog에 띄우지 않기 위해 bool형 변수를 반환합니다.

void Sender(string s)

서버로 메세지를 보내는 메소드입니다. 사실 Disconnect() 메소드의 전송부분도 Sender로 대체하면 되긴합니다. 새벽에 만드느라 살짝 엉성한 부분이 많네요

void Controller(string s)

InputField에서 메세지를 받아 컨트롤 메세지라면 맞는 처리를, 그렇지 않으면 서버에 그대로 전송합니다.

void Disconnet()

폼을 닫거나 나가기 버튼을 누를 때 서버와 연결을 끊습니다. 

void InputField_Keydown(object sender, KeyEventargs e)

키보드의 엔터키 입력시 EnterBtn_Click()메소드를 실행합니다.

void EnterBtn_Click(object sender, Eventargs e)

InputField의 텍스트를 비우고 Controller(해당 텍스트) 메소드를 실행합니다.

후에 InputField에 다시 포커스를 맞춥니다.

 

void RefreshUserlist()와 void ClientForm_Load()는 사용하지않았습니다.

예외가 일어나는 부분이 꽤 많습니다... VS 디버그 모드로 해당부분 예외 발견 후 처리하는 구문을 많이 쓰셔야 할 수 있습니다.

 

 

복사가능한 전체 코드는 아래에 있습니다.

더보기

EnterID.cs

using System;
using System.Windows.Forms;

namespace myClient
{
    public partial class EnterID : Form
    {
        string ID;

        public EnterID()
        {
            InitializeComponent();
        }

        private void IDbtn_Click(object sender, EventArgs e)
        {
            if(IDInputField.Text.Equals(string.Empty))
            {

            }
            else
            {
                ID = IDInputField.Text;
                ClientForm clientform = new ClientForm(ID);
                this.Hide();
                clientform.ShowDialog();
                
            }
        }

        private void IDInputField_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Return) IDbtn_Click(sender, e);
        }
    }
}

 

ClientForm.cs

 

using System;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;
using System.Net.Sockets;
using System.Threading;

namespace myClient
{
    public partial class ClientForm : Form
    {
        string ID;
        NetworkStream stream = default(NetworkStream);
        TcpClient client = new TcpClient();
        public ClientForm(string ID)
        {
            this.ID = ID;
            InitializeComponent();
        }
        void Connect(string s)
        {
            char split = ' ';
            string[] parsedIP = s.Split(split); // 1 : client, 2 : server, 3 : server Port
            
            try
            {
                IPEndPoint clientAddr = new IPEndPoint(IPAddress.Parse(parsedIP[1]), 0);
                IPEndPoint serverAddr = new IPEndPoint(IPAddress.Parse(parsedIP[2]), Int32.Parse(parsedIP[3]));
                client = new TcpClient(clientAddr);
                client.Connect(serverAddr);
                stream = client.GetStream();
                Display("Chat Server Connected...");
            }
            catch(Exception e)
            {
                Display("Failed to Connect...");
            }
            if(!client.Connected)
            {
                MessageBox.Show("usage : /connect 클라이언트IP 서버IP 서버PORT");
                InputField.Text = string.Empty;
                InputField.Focus();
            }
            else
            {
                byte[] buffer = Encoding.Default.GetBytes(ID);
                stream.Write(buffer, 0, buffer.Length);

                Thread clientThread = new Thread(Receiver);
                clientThread.IsBackground = true;
                clientThread.Start();
            }
            
        }
        void Display(string msg)
        {
            CheckForIllegalCrossThreadCalls = false;
            ChatLog.AppendText(msg+"\r\n");
            ChatLog.Select(ChatLog.Text.Length, 0);
            ChatLog.ScrollToCaret();
        }
        void Receiver()
        {
            try
            {
                while (true)
                {
                    int bufferSize = client.ReceiveBufferSize;
                    byte[] buffer = new byte[bufferSize];
                    int bytes = stream.Read(buffer, 0, buffer.Length);
                    string msg = Encoding.Default.GetString(buffer, 0, bytes);
                    if(!isControlMsg(msg)) Display(msg);
                }
            }
            catch(IOException e)
            {
                //
            }
        }
        bool isControlMsg(string msg)
        {
            bool isCtrl = true;
            if (msg.StartsWith("**userlist** "))
            {
                ChatterList.Items.Clear();
                char split = ' ';
                string[] userlist = msg.Split(split);
                foreach(string s in userlist)
                {
                    if (s.StartsWith("**")) continue;
                    ChatterList.Items.Add(s);
                }
            }
            else isCtrl = false;
            return isCtrl;
        }
        void RefreshUserlist(string[] ul)
        {

        }
        void Sender(string s)
        {
            byte[] buffer = Encoding.Default.GetBytes(s);
            stream.Write(buffer, 0, buffer.Length);
            stream.Flush();
        }
        void Controller(string s)
        {
            if(s.StartsWith("/connect "))
            {
                Connect(s);
            }
            else if(!client.Connected)
            {
                MessageBox.Show("서버에 먼저 연결해주세요.");
                InputField.Text = string.Empty;
                InputField.Focus();
            }
            else if(s.Equals("/exit"))
            {
                Disconnect();
            }
            else
            {
                Sender(s);
            }
        }
        void Disconnect()
        {
            byte[] buffer = Encoding.Default.GetBytes("/exit");
            stream.Write(buffer, 0, buffer.Length);
            stream.Flush();
            stream.Close();
            client.Close();
            Application.ExitThread();
            this.Close();
        }
        void Disconnect(object sender, EventArgs e)
        {
            byte[] buffer = Encoding.Default.GetBytes("/exit");
            stream.Write(buffer, 0, buffer.Length);
            stream.Flush();
            stream.Close();
            client.Close();
            Application.ExitThread();
            this.Close();
        }
        private void InputField_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Return) EnterBtn_Click(sender, e);
        }

        private void EnterBtn_Click(object sender, EventArgs e)
        {
            string input = InputField.Text;
            Controller(input);
            InputField.Text = string.Empty;
            InputField.Focus();
        }

        private void ClientForm_Load(object sender, EventArgs e)
        {

        }
    }
}

서버쪽 창입니다. 서버에서 여러가지 컨트롤을 할 수 있게끔 입력창을 만들었고, 서버의 상태를 확인할 수 있게끔 콘솔창을 넣었습니다. 오른쪽 리스트박스는 채팅 참가자 리스트를 실시간으로 반영합니다.

 

0. 전체 폼

취향에 맞게 꾸미셔도 됩니다. Load시 ChatServerForm_Load() 메소드를 실행하게끔 합니다.

1. 콘솔창(RichTextBox, name : myConsole)

콘솔창을 RichTextBox로 만든 이유는 너무 길어지면 스크롤바를 추가하게끔 하기 위해서입니다. 코드 내 이름은 myConsole이고, multiline = true, readonly = true로 하시면 되겠습니다. 크기는 취향껏

2. 채팅참가자 목록(ListBox, name : ClientList)

굳이 실시간으로 반영하지 않고 명령어로 물어볼 때만 출력하게끔 할까 하다가 그냥 하는김에 실시간으로 반영하고 클라이언트들에게도 전송하게끔 했습니다. 그 과정에 쓰레드를 하나 더 생성해버렸지만

3. 입력창(TextBox, name : ControlInput)

여러 명령어들을 입력할 입력창입니다. 굳이 다른 옵션 건드릴 필요가 없습니다.

KeyDown시 ControlInput_KeyDown() 메소드를 실행하게끔 추가합니다.

4. 전송버튼(Button, name : Enter)

전송버튼입니다. 키보드의 엔터키를 눌러도 작동하게끔 만들었습니다.

Click시 ControlEnter() 메소드를 실행합니다.

 

ChatServerForm.cs 코드는 아래와 같습니다.

메소드 하나하나 살펴보자면

 

Addtext(string text)

myConsole에 text를 이어붙입니다. AppendText 아래엔 스크롤바를 자동으로 아래로 내리게끔 하는 기능입니다.

ControlEnter(object sender, EventArgs e)

Enter버튼 입력시 실행되는 메소드입니다. ControlInput 텍스트박스에 입력한 문자열을 Controller 메소드로 옮겨 실행하고 텍스트박스를 비운 후 포커스를 다시 텍스트박스로 옮깁니다.

RefreshChatters()

채팅 참가자 리스트를 리스트박스에 실시간으로 업데이트합니다. (1초간격)

bufferedList는 각 클라이언트에 참가자 리스트를 전송하기 위해 만들었습니다. 앞에 컨트롤 메시지라는 "**userlist** "를 동봉했습니다. 후에 MyChatServer 클래스에서 이를 전송합니다.

CheckForIllegalCrossThreadCalls = false; 를 추가해 크로스 스레드 예외를 무시했습니다. 어차피 이 자원을 사용하는 스레드는 하나밖에 없습니다.

OpenServer(object s) 

/open [ServerIP] 명령어 입력시 실행됩니다. 문자열s로부터 "xxx.xxx.xxx.xxx"형태의 IP주소를 얻고 이를 IPAddress.Parse()를 통해 IP주소값으로 변환, 서버를 엽니다. 포트번호는 제 맘대로 넣었습니다...

client = server.AcceptTcpClient() 를 통해 클라이언트를 계속 받고, 받을 때마다 해당 클라이언트를 처리하는 서버 쓰레드를 만들어 Listen() 메소드를 실행합니다.

이때 서버 쓰레드에 해당 클라이언트의 TcpClient와 ID를 넘겨주며, 폼을 컨트롤 할 수 있게끔 this도 전달해줍니다.

예외 발생시 break;라 했지만 보다 섬세한 예외처리를 하시는게 더 좋을거같습니다.

vold CloseServer()

구현은 안했지만 /close 입력시 실행되는 메소드입니다. 서버를 닫아주고 그에 따른 처리를 하시면 됩니다.

Controller(string s)

ControlEnter 메소드에서 문자열을 받아 처리하는 메소드입니다.

본문에선 지웠지만 금지어를 추가하는

            else if (s.StartsWith("/spam "))
            {
                string spam = s.Substring("/spam ".Length);
                if (spams.Contains(spam))
                {
                    AddText(string.Format("[Server] Spam {0} is already banned.\r\n", spam));
                }
                else
                {
                    spams.Add(spam);
                    AddText(string.Format("[Server] Spam {0} Added.\r\n", spam));
                }
            }

등의 조건문이 있었습니다. 취향껏 여러 조건문을 덧붙이셔도 됩니다.

ControllInput_KeyDown(object sender, KeyEventArgs e)

키보드의 엔터키(Keys.Return) 입력 시에도 ControlEnter() 메소드를 호출합니다.

ChatServerForm_Load(object sender, EventArgs e)

폼 생성과 동시에 채팅 참가자 리스트를 갱신하는 쓰레드를 만들어 실행했습니다.

 

 

 

각 클라이언트와 통신을 담당할 MyChatServer.cs 코드입니다.

public MyChatServer(client, userID, serverForm)

이 객체는 생성자로 TcpClient, userID, serverForm을 받습니다.

생성시 서버 콘솔에 해당 user의 접속사실을 띄우고 모든 유저에게 접속을 알립니다.

그리고 쓰레드를 생성해 Refresh() 메소드를 실행시킵니다.

Refresh()

이 메소드는 1초 간격으로 클라이언트에게 채팅 참가자 리스트를 "**userlist** user1 user2 ..." 형식으로 전송합니다.

클라이언트는 이 메세지를 받으면 유저들을 본인들의 ListBox에 대입시킵니다.

Broadcast(string msg, string ID)

모든 유저에게 메세지를 전달합니다. [ID] 메세지 형식입니다. 공지를 하고싶은 경우 ID에 "Notice"를 넣었습니다.

bool Controller(string str)

클라이언트에게 받은 메세지가 채팅이 아닌 컨트롤 메세지임을 판단합니다. "/exit"나 본문엔 없지만 "/spamlist" 등으로 금지어 목록을 요청하는 경우 해당 메세지를 Broadcast하는게 아닌 서버에서 따로 메세지를 만들어 해당 유저에게 보내도록 합니다.

void Listen()

계속해서 클라이언트에게 오는 메세지를 받습니다. 연결상태를 확인해 연결중이 아니라면 종료합니다.

마찬가지로 취향껏 예외처리를 하시면 되겠습니다.

 

아래는 복사 가능한 전체 코드입니다.

 

더보기

MyChatServer.cs

 

using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net.Sockets;
using System.Threading;

namespace chattingServer
{
    class MyChatServer
    {
        static Dictionary<string, TcpClient> userData = new Dictionary<string, TcpClient>();
        TcpClient client;
        NetworkStream stream = default (NetworkStream);
        string userID;
        ChatServerForm serverForm;
        public MyChatServer(TcpClient client, string userID, ChatServerForm serverForm)
        {
            this.userID = userID;
            this.client = client;
            this.serverForm = serverForm;
            userData.Add(userID, client);
            serverForm.AddText(string.Format($"[{userID}] connected to server.\r\n"));
            Broadcast(string.Format("{0} joined to server", userID), "Notice");
            Thread refresher = new Thread(Refresh);
            refresher.IsBackground = true;
            refresher.Start();
        }
        void Refresh()
        {
            while(true)
            {
                if (!client.Connected) break;
                byte[] bytes = Encoding.Default.GetBytes(serverForm.bufferedList);
                stream.Write(bytes, 0, bytes.Length);
                stream.Flush();
                Thread.Sleep(1000);
            }

        }
        void Broadcast(string msg, string ID)
        {
            foreach (var user in userData)
            {
                TcpClient client = user.Value as TcpClient;
                stream = client.GetStream();
                byte[] buffer = Encoding.Default.GetBytes(string.Format("[{0}] {1}", ID, msg));
                stream.Write(buffer, 0, buffer.Length);
                stream.Flush();
            }
        }
        bool Controller(string str)
        {
            bool isControlMsg = true;
            if (str.Equals("/exit"))
            {
                serverForm.users.Remove(userID);
                Broadcast(string.Format($"{userID} exited"), "Notice");
                userData.Remove(userID);
            }
            else
            {
                isControlMsg = false;
            }
            return isControlMsg;
        }
        public void Listen()
        {
            NetworkStream stream = client.GetStream();
            try
            {
                while (true)
                {
                    int bufLength;
                    byte[] buffer = new byte[1024];
                    string str = string.Empty;
                    if (!client.Connected) break;
                    try
                    {
                        while ((bufLength = stream.Read(buffer, 0, buffer.Length)) != 0)
                        {
                            str = Encoding.Default.GetString(buffer, 0, bufLength);
                            serverForm.AddText(string.Format($"[{userID}] {str}\r\n"));
                            if (!Controller(str)) Broadcast(str, userID);
                        }
                    }
                    catch (IOException e)
                    {
                    }

                }
            }
            catch (SocketException e)
            {
                serverForm.AddText(string.Format("{0} 클라이언트 오류로 인한 퇴장\r\n", userID));
                stream.Close();
                client.Close();
            }
            finally
            {
                serverForm.AddText(string.Format("{0} 클라이언트 퇴장\r\n", userID));
                stream.Close();
                client.Close();
            }


        }
    }
}

 

 

ChatServerForm.cs

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;

namespace chattingServer
{
    public partial class ChatServerForm : Form
    {
        public string bufferedList;
        public List users = new List();
        TcpListener server;
        public void AddText(string text)
        {
            myConsole.AppendText(text);
            myConsole.Select(myConsole.Text.Length, 0);
            myConsole.ScrollToCaret();
        }
        void ControlEnter(object sender, EventArgs e)
        {
            string input = ControlInput.Text;
            Controller(input);
            ControlInput.Text = string.Empty;
            ControlInput.Focus();
        }
        void RefreshChatters()
        {
            CheckForIllegalCrossThreadCalls = false;
            while (true)
            {
                bufferedList = "**userlist** ";
                ClientList.Items.Clear();   
                foreach(string user in users)
                {
                    ClientList.Items.Add(user);
                    bufferedList += (user+" ");
                }
                Thread.Sleep(1000);
            }
        }
        void OpenServer(object s)
        {
            string serverIP = s.ToString().Substring("/open ".Length);
            TcpClient client = new TcpClient();
            const int Port = 10203;
            IPEndPoint serverAddr = new IPEndPoint(IPAddress.Parse(serverIP), Port);
            server = new TcpListener(serverAddr);
            try
            {
                server.Start();
            }
            catch (SocketException e)
            {
                MessageBox.Show("올바르지 않은 주소입니다.");
                return;
            }
            AddText(String.Format("Server Opened. [{0}]\r\n", serverAddr.ToString()));
            while (true)
            {
                try
                {
                    client = server.AcceptTcpClient();
                    NetworkStream stream = client.GetStream();
                    byte[] buffer = new byte[1024];
                    int bytes = stream.Read(buffer, 0, buffer.Length);
                    string userID = Encoding.Default.GetString(buffer, 0, bytes);
                    users.Add(userID);
                    MyChatServer mychat = new MyChatServer(client, userID, this);
                    Thread serverThread = new Thread(new ThreadStart(mychat.Listen));
                    serverThread.IsBackground = true;
                    serverThread.Start();
                }
                catch (Exception e) { break; }
            }
            client.Close();
            server.Stop();
        }
        void CloseServer()
        {
            // 서버 닫는 처리
        }
        void Controller(string s)
        {
            if (s.Equals(string.Empty)) return;
            else if(s.Equals("/close"))
            {
                CloseServer();
                AddText("[Server] Closed.\r\n");
            }
            else if (s.StartsWith("/open "))
            {
                Thread open = new Thread(OpenServer);
                open.IsBackground = true;
                open.Start(s);
            }
        }
        public ChatServerForm()
        {
            InitializeComponent();
        }
        private void ControlInput_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Return) ControlEnter(sender, e);
        }

        private void ChatServerForm_Load(object sender, EventArgs e)
        {
            Thread refresher = new Thread(RefreshChatters);
            refresher.IsBackground = true;
            refresher.Start(); 
        }
    }
}

 

 

+ Recent posts