求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
WPF+WCF一步一步打造音频聊天室(一)
 

2011-4-15 来源:网络

 

WPF+WCF一步一步打造音频聊天室(1):概述

前几天在老徐的MSN群中聊WCF。我突然想到了用WPF和WCF来打造音频聊天室这么一个轮子。其实是这样的,我在的公司是两岸三地合作开发的。两岸是指大陆和台湾,三地是指深圳、台北、高雄。由于公司很小,所以一切都要节约成本,当然特别是我们深圳的开发团队,目前只有3个人,其中两个开发人员,一个Leader。现在我们两岸三地之间的沟通就是使用skype的,因为貌似其他的软件都要有软硬件的支持。skype只要每个成员有个带麦克风的耳机就行了。

平时,我们深圳这边讨论问题,有一个很大的白板可以一边讲,一边写,一边讨论,还是不错的。但是Skype貌似没有共享写字板的功能,我们写的东西,台北和高雄那边看不到。由于我们项目开发基本是是使用WF、WPF以及WCF,故我自己打算用业余时间用WPF和WCF实现这么一个简单的会议软件,并计划一步一步完善,并放到Codeplex上面。我将使用三篇文章对现在这个程序进行简单的叙述,其实现在写这个程序,我只花了不到一天的时间。之所以打算用三个篇幅来写,想就算你从来没有接触过WPF和WCF也能看明白。

在开始之前,我想说明白一件事情就是‘造轮子’。

造轮子是贬义词,诚然搞这个东西确实是在造轮子,这里我写几点点对我来说造轮子的好处:

  • 1、自然是为了练好技术
  • 2、我将实现的方式一步一步写出来,自然是为了分享知识
  • 3、学习优秀产品的设计(QQ、MSN)。

故我从不鄙视造轮子。腾讯靠的就是‘造轮子’,好了,废话不多说了,让我开始吧。

第一篇只是一个简单的叙述,没有任何代码,和具体的实现。

首先提出需求:

  • 1、文字聊天
  • 2、共享一个讨论问题的白板
  • 3、能进行语音通话
  • 4、聊天室成员查看
  • 5、允许私聊
  • 6、用户进入/离开聊天室的广播通知
  • 8、聊天记录的保存
  • 9、视频(这个暂且没有去实现).

使用技术:

1、WPF

2、WCF

开发工具: VS2010

功能分析

现在列出的功能非常的简单,界面使用WPF。通信使用WCF,WCF可以有四个方法:

1、传输文字

2、传输白板图像

3、传输声音

4、传输视频

为了简化起见,我打算将聊天记录在本地的XML中,MSN采用的是这种方式

最初版本的截图效果:

1、登陆:

主面板

上图中的成员列表:当前聊天室的成员

上图中工具栏:包括一些常用的操作

上图中白板区域:共享的白板

上图中文字聊天区域:消息显示框和消息输入框

WPF+WCF一步一步打造音频聊天室(2):文字聊天和白板共享

这篇文章将讲述实现WPF的UI和WCF中的双工通信。实现文字部分的聊天功能和实现共享白板的功能。

画WPF的界面其实是一件麻烦的事情。虽然WPF和WindowsForm一样,能将控件拖到哪,它就在哪。我们在开发asp.net项目的时候用从原始的table布局,到现在流行的div+css布局。这些都需要设计人员的仔细设计。这个程序的布局我采用Grid和StackPanel两种方式。Gird类似html的表格布局,StackPanel就就像它的字面意思“堆栈面板”。

首先新建一个wpf的应用程序,改名为ZqlChart。添加一个UserControl,用来实现登陆窗体,这个是用了StackPanel进行布局。XAML代码如下:

<UserControl x:Class="ZqlChart.LoginControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="210" Width="350" Loaded="UserControl_Loaded">
    <StackPanel>
        <Border Height="220" BorderBrush="#FFFFFFFF" BorderThickness="2,2,2,0" CornerRadius="5,5,0,0">
            <Border.Background>
                <LinearGradientBrush EndPoint="0.713,0.698" StartPoint="0.713,-0.139">
                    <GradientStop Color="#FFFFFFFF" Offset="0.933"/>
                    <GradientStop Color="LightBlue" Offset="0.337"/>
                </LinearGradientBrush>
            </Border.Background>
            <StackPanel Name="infoPanel" Orientation="Vertical" Margin="10,10,10,10">
                <StackPanel Name="typePanel" Orientation="Horizontal">
                    <RadioButton Name="chatTypeServer" FontSize="24" Margin="80,0,20,0" 
                                 Checked="chatTypeServer_Checked" VerticalContentAlignment="Center">服务端</RadioButton>
                    <RadioButton Name="chatTypeClient" FontSize="24" Checked="chatTypeClient_Checked" 
VerticalContentAlignment="Center">客户端</RadioButton>
                </StackPanel>
                <StackPanel Name="serverPanel" Orientation="Horizontal" Margin="0,10,0,0">
                    <Label Name="lblServer" FontSize="20" Width="120" HorizontalContentAlignment="Right"
 VerticalContentAlignment="Center">服务端:</Label>
                    <TextBox Height="30" Name="txtServer" Width="160" FontSize="20" VerticalContentAlignment="Center" />
                </StackPanel>
                <StackPanel Name="usernamePanel" Orientation="Horizontal" Margin="0,10,0,10">
                    <Label Name="lblUserName" FontSize="20" Width="120" HorizontalContentAlignment="Right">用户名:</Label>
                    <TextBox Height="30" Name="txtUserName" Width="160" FontSize="20" VerticalContentAlignment="Center" />
                </StackPanel>
                <StackPanel Name="buttonPanel" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
                    <Button Name="btnLogin" Width="120" FontSize="20" Margin="10,10,10,10" Click="btnLogin_Click">连接</Button>
                    <Button Name="btnCancel" Width="120" FontSize="20" Margin="10,10,10,10" Click="btnCancel_Click">取消</Button>
                </StackPanel>
            </StackPanel>
        </Border>

    </StackPanel>
</UserControl>

界面效果如下:

聊天的主界面,如下图:

大框架是3行3列。XAML代码如下:

<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="ZqlChartMainWindow"
 	x:Class="ZqlChart.ZqlChartWindow" 
    Title="麒麟语音聊天室” Height="600" Width="800"
    Background="#FF3B3737" Loaded="Window_Loaded" MinWidth="800" MinHeight="500">
    <Grid x:Name="LayoutRoot" >
        <Grid.RowDefinitions>
        
            <RowDefinition Height="50" />
            <RowDefinition Height="261" />
            <RowDefinition Height="250" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150" />
            <ColumnDefinition Width="580*" />
            <ColumnDefinition Width="48" />
        </Grid.ColumnDefinitions>

        <Border Name="BorderUsersList" Grid.Column="0" Grid.Row="1" Grid.RowSpan="2" CornerRadius="8,8,8,8"
 Background="LightBlue" BorderThickness="0,0,4,4" >
            <ListView Name="lvUsers" Margin="10" FontSize="20">
                <ListView.BitmapEffect>
                    <DropShadowBitmapEffect />
                </ListView.BitmapEffect>
            </ListView>
        </Border>

        <Border Name="BorderEditingType" Grid.ColumnSpan="3" CornerRadius="8,8,8,8" Background="LightBlue" BorderThickness="0,4,4,4">
                <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
                <Button Margin="0,0,0,0"  Height="28" Width="121" Click="Button_Click" Background="White">
                  与其语音聊天
                </Button>
                <RadioButton Name="rbInk" Content="墨水" Margin="15,0,0,0" VerticalAlignment="Center" FontSize="20" IsChecked="True" 
                                 Tag="{x:Static InkCanvasEditingMode.Ink}" Click="rbInkType_Checked">
                    </RadioButton>
                <RadioButton Name="rbEraserByStroke" Content="一笔一笔清除" Margin="15,0,0,0" VerticalAlignment="Center" FontSize="20" 
                                 Tag="{x:Static InkCanvasEditingMode.EraseByStroke}" Click="rbInkType_Checked">
                    </RadioButton>
                <RadioButton Name="rbEraserByPoint" Content="一点一点清除" Margin="15,0,0,0" VerticalAlignment="Center" FontSize="20" 
                                 Tag="{x:Static InkCanvasEditingMode.EraseByPoint}" Click="rbInkType_Checked">
                    </RadioButton>
                    <TextBlock Margin="25,0,10,0" VerticalAlignment="Center" FontSize="20" >选择颜色:</TextBlock>
                    <Button Margin="0,0,0,0" Background="White" Height="28" Width="64" Click="OnSetFill">
                        <Rectangle Width="54" Height="20" Stroke="Black" StrokeThickness="2">
                            <Rectangle.Fill>
                                <SolidColorBrush Color="{Binding ElementName=ZqlChartMainWindow, Path=FillColor}" />
                            </Rectangle.Fill>
                        </Rectangle>
                    </Button>
                    
            </StackPanel>
        </Border>

        <Border Name="BorderInkCanvas" Grid.Column="1" Grid.Row="1" Background="LightBlue" BorderThickness="4,4,4,4"
 CornerRadius="8,8,8,8" Grid.ColumnSpan="2">
            <InkCanvas x:Name="inkCanv" Margin="10" Background="White" 
                        StrokeCollected="inkCanv_StrokeCollected" StrokeErasing="inkCanv_StrokeErasing" 
                       StrokeErased="inkCanv_StrokeErased" VerticalAlignment="Top" >
            </InkCanvas>
        </Border>

        <Border Name="BorderInkMessage" Grid.Column="1" Grid.Row="2" Background="LightBlue" BorderThickness="0,0,4,4"
 CornerRadius="8,8,8,8" Grid.ColumnSpan="2">
            <Grid >
                <Grid.RowDefinitions>
                    <RowDefinition />
                    <RowDefinition  Height="30" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="500*" />
                    <ColumnDefinition Width="62*" />
                    <ColumnDefinition Width="62*" />
                </Grid.ColumnDefinitions>
                <Border  Grid.Column="1" Grid.Row="1" Background="LightBlue" BorderThickness="4,4,4,4" CornerRadius="8,8,8,8" >
                    <Button Content="发送"  Height="23"  Name="btnSend"  Click="btnSend_Click" />
                </Border>
                <Border  Grid.ColumnSpan="3" Background="LightBlue" BorderThickness="4,4,4,4" CornerRadius="8,8,8,8" >
                    <TextBox   Name="txtAllMessage" >
                        <TextBox.BitmapEffect>
                            <DropShadowBitmapEffect />
                        </TextBox.BitmapEffect>
                    </TextBox>
                </Border>
                <Border  Grid.Row="1" Background="LightBlue" BorderThickness="4,4,4,4" CornerRadius="8,8,8,8" >
                    <TextBox Grid.Row="1" Name="txtMessage" />
                </Border>
                <Border  Grid.Column="2" Grid.Row="1"  Background="LightBlue" BorderThickness="4,4,4,4" CornerRadius="8,8,8,8" >
                    <Button  Content="关闭" Name="btnLeave"  Height="23" FontSize="10" Click="btnLeave_Click">

                    </Button>
                </Border>
            </Grid>
        </Border>
        <Canvas Name="loginCanvas" Grid.Column="1" Grid.Row="1" Width="500" Height="300" VerticalAlignment=
"Top" HorizontalAlignment="Center" Margin="39,78,41,0" Grid.RowSpan="2" />
    </Grid>
</Window>

窗体就设计好了。WCF双工通信: 双工通信能允许服务通知用户当前的进度情况。我们可以通过使用指定CallbackContract的ServiceContract属性的服务使用双工,如服务器端的代码如下:

[ServiceContract(CallbackContract = typeof(IService1Callback))]
public interface IService1
{
    [OperationContract]
    string GetData(int value);
}
 
[ServiceContract]
public interface IService1Callback
{
    [OperationContract]
    void Reply(string message);
}

客户端代码:

class Program
{
    static void Main(string[] args)
    {
        var callback = new Service1Callback();
        var proxy = new Service1Client(new InstanceContext(callback));
        Console.WriteLine(proxy.GetData(42));
        Console.ReadLine();
    }
}
 
class Service1Callback : IService1Callback
{
    public void Reply(string message)
    {
        Console.WriteLine(message);
    }
}

这篇文章中我将利用WCF的双工通信实现文字聊天的功能和共享白板的功能。

定义协议:

public interface IZqlChartService
    {
        [OperationContract()]
        bool Join(ChatUser chatUser);

        [OperationContract()]
        void Leave(ChatUser chatUser);

        [OperationContract]
        void SendBroadcastMessage(string strUserName, string message);

        [OperationContract()]
        bool IsUserNameTaken(string strUserName);

        [OperationContract()]
        void SendInkStrokes(MemoryStream memoryStream);


    }

定义回调:
    public interface IZqlChartServiceCallback
    {
        [OperationContract(IsOneWay = true)]
        void NotifyMessage(string message);

        [OperationContract(IsOneWay = true)]
        void UpdateUsersList(List<ChatUser> listChatUsers);

        [OperationContract(IsOneWay = true)]
        void OnInkStrokesUpdate(ChatUser chatUser, byte[] bytesStroke);

        [OperationContract(IsOneWay = true)]
        void ServerDisconnected();

    }
实现服务:
    public class ZqlChartService : IZqlChartService
    {
        public static Dictionary<IZqlChartServiceCallback, ChatUser> s_dictCallbackToUser 
= new Dictionary<IZqlChartServiceCallback, ChatUser>();

        public ZqlChartService()
        {
        }

        public bool Join(ChatUser chatUser)
        {
            IZqlChartServiceCallback client = OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>();

            if (s_dictCallbackToUser.ContainsValue(chatUser) == false)
            {
                s_dictCallbackToUser.Add(client, chatUser);
            }

            foreach (IZqlChartServiceCallback callbackClient in s_dictCallbackToUser.Keys)
            {
                callbackClient.UpdateUsersList(s_dictCallbackToUser.Values.ToList());
            }

            return true;
        }

        public void Leave(ChatUser chatUser)
        {
            IZqlChartServiceCallback client = OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>();
            if (s_dictCallbackToUser.ContainsKey(client))
            {
                s_dictCallbackToUser.Remove(client);
            }

            foreach (IZqlChartServiceCallback callbackClient in s_dictCallbackToUser.Keys)
            {
                if (chatUser.IsServer)
                {
                    if (callbackClient != client)
                    {
                        //server user logout, disconnect clients
                        callbackClient.ServerDisconnected();
                    }
                }
                else
                {
                    //normal user logout
                    callbackClient.UpdateUsersList(s_dictCallbackToUser.Values.ToList());
                }
            }

            if (chatUser.IsServer)
            {
                s_dictCallbackToUser.Clear();
            }
        }

        public bool IsUserNameTaken(string strNickName)
        {
            foreach (ChatUser chatUser in s_dictCallbackToUser.Values)
            {
                if (chatUser.NickName.ToUpper().CompareTo(strNickName) == 0)
                {
                    return true;
                }
            }
            return false;
        }

        public void SendInkStrokes(MemoryStream memoryStream)
        {
            IZqlChartServiceCallback client = OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>();

            foreach (IZqlChartServiceCallback callbackClient in s_dictCallbackToUser.Keys)
            {
                if (callbackClient != OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>())
                {
                    callbackClient.OnInkStrokesUpdate(s_dictCallbackToUser[client], memoryStream.GetBuffer());
                }
            }
        }

        public void SendBroadcastMessage(string clientName, string message)
        {
            IZqlChartServiceCallback client =
                OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>();

            if (client != null)
            {

                foreach (IZqlChartServiceCallback callbackClient in s_dictCallbackToUser.Keys)
                {
                    if (callbackClient != OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>())
                    {
                        callbackClient.NotifyMessage(clientName + ": " + message);
                    }
                }
            }
        }

    }

客户端:

    [System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
    public partial class ZqlChartServiceClient : System.ServiceModel.DuplexClientBase<IZqlChartService>, IZqlChartService
    {

        public ZqlChartServiceClient(System.ServiceModel.InstanceContext callbackInstance) :
            base(callbackInstance)
        {
        }

        public ZqlChartServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) :
            base(callbackInstance, endpointConfigurationName)
        {
        }

        public ZqlChartServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) :
            base(callbackInstance, endpointConfigurationName, remoteAddress)
        {
        }

        public ZqlChartServiceClient(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, 
System.ServiceModel.EndpointAddress remoteAddress) :
            base(callbackInstance, endpointConfigurationName, remoteAddress)
        {
        }

        public ZqlChartServiceClient(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, 
System.ServiceModel.EndpointAddress remoteAddress) :
            base(callbackInstance, binding, remoteAddress)
        {
        }

        public bool Join(ZqlChartObjects.ChatUser chatUser)
        {
            return base.Channel.Join(chatUser);
        }

        public void Leave(ZqlChartObjects.ChatUser chatUser)
        {
            base.Channel.Leave(chatUser);
        }

        public bool IsUserNameTaken(string strUserName)
        {
            return base.Channel.IsUserNameTaken(strUserName);
        }

        public void SendInkStrokes(System.IO.MemoryStream memoryStream)
        {
            base.Channel.SendInkStrokes(memoryStream);
        }

        public void SendBroadcastMessage(string strUserName, string message)
        {
            base.Channel.SendBroadcastMessage(strUserName, message);
        }



    }

客户端回调类:

public class ClientCallBack : IZqlChartServiceCallback
    {
        public static ClientCallBack Instance;
        private SynchronizationContext m_uiSyncContext = null;
        private ZqlChartWindow m_mainWindow;
        //ActiveCallWindow _activeCallForm;
        //CallManager _callManager;
        public ClientCallBack(SynchronizationContext uiSyncContext, ZqlChartWindow mainWindow)
        {
            m_uiSyncContext = uiSyncContext;
            m_mainWindow = mainWindow;
        }

        public void OnInkStrokesUpdate(ZqlChartObjects.ChatUser chatUser, byte[] bytesStroke)
        {
            SendOrPostCallback callback =
                      delegate(object state)
                      {
                          m_mainWindow.OnInkStrokesUpdate(state as byte[] );
                      };

            m_uiSyncContext.Post(callback, bytesStroke);

            SendOrPostCallback callback2 =
                      delegate(object objchatUser)
                      {
                          m_mainWindow.LastUserDraw(objchatUser as ZqlChartObjects.ChatUser);
                      };
            m_uiSyncContext.Post(callback2, chatUser);
        }

        public void UpdateUsersList(List<ZqlChartObjects.ChatUser> listChatUsers)
        {
            SendOrPostCallback callback =
                     delegate(object objListChatUsers)
                     {
                         m_mainWindow.UpdateUsersList(objListChatUsers as List<ZqlChartObjects.ChatUser>);
                     };

            m_uiSyncContext.Post(callback, listChatUsers);
        }

        public void ServerDisconnected()
        {
            SendOrPostCallback callback =
                        delegate(object dummy)
                        {
                            m_mainWindow.ServerDisconnected();
                        };

            m_uiSyncContext.Post(callback, null);
        }
        public void NotifyMessage(string message)
        {
            SendOrPostCallback callback =
                              delegate(object dummy)
                              {
                                  m_mainWindow.NotifyMessage(message);
                              };

            m_uiSyncContext.Post(callback, message);
        }


        public bool AcceptCall(string username)
        {
            //调獭?用?线?程ì必?须?为a STA,?因皑?为a许í多à UI 组哩?件t都?需è要癮。£
            return MessageBox.Show(String.Format("Accep call from \"{0}\" ", username), "Incomming Call", 
MessageBoxButton.YesNo, MessageBoxImage.Information) == MessageBoxResult.Yes;
        }

    }

效果:1、服务端登陆:

2、客户端登录:

3、文字聊天

4、共享白板

这篇文章实现了WPF的UI界面以及文字聊天和共享白板的功能。

 


使用decj简化Web前端开发
Web开发框架形成之旅
更有效率的使用Visual Studio
MVP+WCF+三层结构搭建框架
ASP.NET运行机制浅析【图解】
编写更好的C#代码
10个Visual Studio开发调试技巧
更多...   


.NET框架与分布式应用架构设计
.NET & WPF & WCF应用开发
UML&.Net架构设计
COM组件开发
.Net应用开发
InstallShield


日照港 .NET Framework & WCF应用开发
神华信息 .NET单元测试
北京 .Net应用软件系统架构
台达电子 .NET程序设计与开发
赛门铁克 C#与.NET架构设计
广东核电 .Net应用系统架构
更多...