windows程序设计课程作业二:音乐播放器

0 作业内容

请编写一个C#程序,实现音乐文件的播放功能。

要求1:

  1. 程序应能够读取MP3文件,并播放其中的音频。
  2. 程序应能够处理可能出现的异常,如文件不存在、文件读取错误等。
  3. 程序应具有良好的用户界面,方便用户进行操作。
  4. 程序应具有良好的兼容性,能在不同版本的C#中正常运行。 提示:此功能可以使用WindowsMediaPlayer控件

要求2:

  1. 程序应能够播放ogg文件。
  2. 程序应能够处理可能出现的异常,如文件不存在、文件读取错误等。
  3. 程序应具有良好的用户界面,方便用户进行操作。
  4. 程序应具有良好的兼容性,能在不同版本的C#中正常运行。 提示:此功能可以使用Nuget程序包中的Naudi.Vorbis控件

1 效果预览

1.1 Home界面

1.1.1 未播放音乐时

未播放音乐时

1.1.2 播放音乐时

播放音乐时

1.2 Playlist界面

1.2.1 导入音乐前

未导入音乐时

1.2.2 导入音乐后

导入音乐后

2 核心功能实现

2.0 说明

由于本项目较为完整地完成了页面UI的设计以及用户交互时的一些逻辑,项目整体代码比较复杂,我这里仅说明该项目的一些核心功能,如暂停、播放等等,其他方面的功能,如界面样式、交互响应等,请参考我github仓库中的完整代码:music-player

2.1 项目结构

项目大致可以分为三个主要部分:Form1、UCHome、UCPlaylist

2.1.1 Form1设计

Form1

Form1主要负责子界面Home、Playlist的切换,程序的关闭和最小化等功能

2.1.2 UCHome设计

UCHome

UCHome主要负责Home界面的功能实现,如暂停、播放、下一首、上一首、停止等

2.1.3 UCPlaylist设计

 UCPlaylist

UCPlaylist主要负责Playlist界面的功能实现,如导入歌曲、选择歌曲等

2.2 Form1核心功能

2.2.1 关闭

// Close ---------------------------------------------------------------
private async void btn_close_Click(object sender, EventArgs e)
{
    UCHome.playbackState = -1;
    axWindowsMediaPlayer1.Ctlcontrols.stop();
    await Task.Delay(100);
    Environment.Exit(0);
}

0 大致逻辑

  • 停止播放——等待——退出

1 为什么不直接退出?

  • 直接退出其实也没什么问题,只是我在这样做时发现一个问题:直接退出后,在下一次重新打开再播放时会有短暂的上次播放时的残留(我是强迫症)。所以我做了一点小处理,就是停止后再退出。

2 为什么要等待?

  • 因为多线程的问题,必须留足时间等待线程完成结束播放的功能再退出,否则依然会有残留。

3 停止播放的逻辑

  • UCHome.playbackState = -1把UCHome中的全局变量playbackState置为-1,代表结束播放的信号,等到负责播放的线程察觉这个变化后就会停止
  • axWindowsMediaPlayer1.Ctlcontrols.stop()控制wmp停止播放

4 为什么有停止信号还要控制wmp停止,是否冗余了?

  • 由于wmp不能播放.ogg格式,所以.ogg需要自行实现控制逻辑,故而全局变量playbackState主要服务于.ogg格式

2.2.2 子界面切换

在这里插入图片描述

// Home ---------------------------------------------------------------
private void clickHomeBtn() {
	// 下面三行为样式变化
    btn_home.BackColor = Color.FromArgb(213, 215, 218);
    btn_home.ForeColor = Color.Black;
    pnl_nav_home.Visible = true;
    
    // 此处为核心逻辑(其他函数照此类推)
    ucHome.Visible = true;
}
private void leaveHomeBtn()
{
    btn_home.BackColor = Color.FromArgb(237, 239, 243);
    btn_home.ForeColor = Color.Gray;
    pnl_nav_home.Visible = false;

    ucHome.Visible = false;
}
private void btn_home_Click(object sender, EventArgs e)
{
    leavePlaylistBtn();
    clickHomeBtn();
}

// Playlist ---------------------------------------------------------------
private void clickPlaylistBtn()
{
    btn_playlist.BackColor = Color.FromArgb(213, 215, 218);
    btn_playlist.ForeColor = Color.Black;
    pnl_nav_palylist.Visible = true;

    ucPlaylist.Visible = true;
}
private void leavePlaylistBtn()
{
    btn_playlist.BackColor = Color.FromArgb(237, 239, 243);
    btn_playlist.ForeColor = Color.Gray;
    pnl_nav_palylist.Visible = false;

    ucPlaylist.Visible = false;
}
private void btn_playlist_Click(object sender, EventArgs e)
{
    leaveHomeBtn();
    clickPlaylistBtn();
}

切换子界面逻辑比较简单,最直观的想法就是点击button时,只让对应的界面可见,其他界面不可见,以点击Home按钮举例:不去关注按钮样式的变化,代入函数后,最关键的代码就是ucPlaylist.Visible = falseucHome.Visible = true,即点击后Playlist界面不可见,Home界面可见。

2.3 UCHome核心功能

2.3.1 播放

private void picPlay_Click(object sender, EventArgs e)
{
	// 下面两个函数为样式变化,这里不作说明
    picPlayDisappear();
    picPauseEmerge();
	// 核心逻辑
    switch (UCPlaylist.option)
    {
        case 0:
            Form1.wmp.Ctlcontrols.play();
            break;
        case 1:
            playbackState = 1;
            break;
    }
}

主要关注核心逻辑,UCPlaylist下的全局变量option用来分别此刻是wmp在播放一般格式的音乐(如mp3、flac等),还是NAudio在播放ogg格式的音乐,如果是前者,则让wmp开始播放;如果是后者则让playbackState置1,表示播放状态,等待播放线程响应后就会开始播放。

2.3.2 暂停

private void picPause_Click(object sender, EventArgs e)
{
	// 判断是否有音乐播放
    switch (UCPlaylist.option) {
        case 0:
            if (Form1.wmp.playState == WMPPlayState.wmppsStopped) return;
            break;
        case 1:
            if (playbackState == -1) return;
            break;
    }
    // 样式变化
    picPauseDisappear();
    picPlayEmerge();
    // 根据option来判断暂停的对象
    switch (UCPlaylist.option) {
        case 0:
            Form1.wmp.Ctlcontrols.pause();
            break;
        case 1:
            playbackState = 0;
            break;
    }
}

大致逻辑和播放一致,只是比播放多了一个判断此刻有没有音乐在播放的逻辑,如果没有音乐播放则直接返回。

2.3.3 停止

public static bool isStoppedManually = false;
public void clickPicStop() {
	// 样式变化
    picPlayDisappear();
    picPauseEmerge();
    labelSongName.Text = string.Empty;
    changeStateText(false);
    // 根据option来判断暂停的对象
    switch (UCPlaylist.option)
    {
        case 0:
            Form1.wmp.Ctlcontrols.stop();
            break;
        case 1:
            playbackState = -1;
            break;
    }
}
private void picStop_Click(object sender, EventArgs e)
{
	// 核心逻辑
    isStoppedManually = true;
    UCPlaylist.ucPlayList.smusicList.SelectedIndices.Clear();
    clickPicStop();
}

clickPicStop()函数的逻辑就不多说了,也是和上面一样。这里主要说明一下isStoppedManually这个全局变量的作用,它是用来分别歌曲是被手动停止的还是播放结束自动停止的。我这里作这个区分主要是为了完善体验,如果是手动停止,则音乐结束,而如果是播放结束自动停止,我做的处理是让其自动播放下一首,这样也比较符合一般音乐软件的直觉。
所以这里的意思就是,点击停止按钮以后,isStoppedManually置true,代表是手动停止(默认为自动停止),然后清空列表中选中的歌曲,最后执行停止播放的逻辑。

2.3.4 下一首

public static bool isNextManually = false;
public void clickNext() {
	// 停止
    clickPicStop();
    // 清空选中歌曲
    UCPlaylist.ucPlayList.smusicList.SelectedIndices.Clear();
    // 当前歌曲索引加1
    ++UCPlaylist.curIndex;
    // 如果是最后一个,则回到第一首
    if (UCPlaylist.curIndex >= UCPlaylist.ucPlayList.smusicList.Items.Count) 			 {
        UCPlaylist.curIndex = 0;
    }
    UCPlaylist.ucPlayList.smusicList.SelectedIndices.Add(UCPlaylist.curIndex);
}
private void picNext_Click(object sender, EventArgs e)
{
	// 如果没有歌曲在播放,直接返回,不进行响应
    switch (UCPlaylist.option)
    {
        case 0:
            if (Form1.wmp.playState == WMPPlayState.wmppsStopped) return;
            break;
        case 1:
            if (playbackState == -1) return;
            break;
    }
    isStoppedManually = false;
    isNextManually = true;
    clickNext();
}

这里逻辑略微有点复杂,我先说明clickNext()函数的逻辑,这个函数的实际作用就是播放下一首歌。要播放下一首歌,就得先将当前播放的歌曲停止,我这里直接调用了上面提到的停止功能,这时候抽离核心函数的作用就体现出来了,代码复用性明显提高。歌曲停止以后,很自然就会想到让播放列表选中下一首歌,所以得先清空播放列表的选中项,然后让负责跟踪歌曲序号的全局变量curIndex加1,最后把curIndex对应的歌曲添加到播放列表的选中项中。
接着说一下点击按钮后的响应,即picNext_Click()的逻辑。首先和暂停功能一样,没有歌曲播放就直接返回。然后因为clickNext()触发了停止功能,而点击下一首并不是手动停止,如果是手动停止的话就也不会播放下一首了,所以让isStoppedManually = false。然后类似于全局变量isStoppedManually的作用,我这里加入了isNextManually全局变量,代表是否是手动播放下一首,因为这里是点击下一首按钮,很明显是手动的,所以让isNextManually = true。最后调用clickNext()就完成了整个下一首的逻辑。

2.3.5 上一首

private void clickPrevious() {
    clickPicStop();
    UCPlaylist.ucPlayList.smusicList.SelectedIndices.Clear();
    --UCPlaylist.curIndex;
    // 如果是第一首,则变成最后一首
    if (UCPlaylist.curIndex < 0)
    {
        UCPlaylist.curIndex = UCPlaylist.ucPlayList.smusicList.Items.Count-1;
    }
    UCPlaylist.ucPlayList.smusicList.SelectedIndices.Add(UCPlaylist.curIndex);
}
private void picPrevious_Click(object sender, EventArgs e)
{
    switch (UCPlaylist.option)
    {
        case 0:
            if (Form1.wmp.playState == WMPPlayState.wmppsStopped) return;
            break;
        case 1:
            if (playbackState == -1) return;
            break;
    }
    isStoppedManually = false;
    isNextManually = true;
    clickPrevious();
}

大致上逻辑没有变动,只是让curIndex从加一变成减一,如果是之前第一首,则点击上一首就会变成最后一首。此外isNextManually不仅仅只是用来区分是否是手动播放下一首歌,上一首也是等效的,毕竟这个变量的重点在于是否是手动的,很明显,点击上一首按钮是手动的。

2.4 UCPlaylist核心功能

2.4.1 导入歌曲

// 存储歌曲文件的url
List<string> urlList = new List<string>();
// 用来去重
HashSet<string> musicSet = new HashSet<string>();

private void buttonAddMusic_Click_1(object sender, EventArgs e)
{
	// 打开资源管理器
    OpenFileDialog ofd = new OpenFileDialog();
    // 设置可多选
    ofd.Multiselect = true;
    // 窗口名称
    ofd.Title = "Select Music File";
    // 后缀过滤
    ofd.Filter = "File Ext | *.mp3;*.avi;*.wav;*.flac;*.ogg";
    if (ofd.ShowDialog() == DialogResult.OK)
    {
    	// 获取选择的音乐文件名称
        string[] nameList = ofd.FileNames;
        // 依次遍历
        for (int i = 0; i < nameList.Length; i++)
        {
        	// 获取当前文件的url
            string url = nameList[i];
            // 如果已存在,则跳过
            if (musicSet.Contains(url)) continue;
            // 开始listview的更新
            musicList.BeginUpdate();
            ListViewItem lvi = new ListViewItem();
            // 格式:序号——歌名——时长——后缀名
            // 添加序号
            lvi.Text = "  " + (cnt++).ToString();
            // 添加歌名
            lvi.SubItems.Add(Path.GetFileNameWithoutExtension(url));
            // 获取后缀名
            string ext = Path.GetExtension(url);
            string duration;
            // 根据后缀名计算歌曲的时长
            switch (ext)
            {
                case ".ogg":
                    duration = getOggMusicDuration(url);
                    break;
                default:
                    duration = getCommonMusicDuration(url);
                    break;
            }
            // 添加时长
            lvi.SubItems.Add(duration);
            // 添加后缀
            lvi.SubItems.Add(ext);
            // 将这一整行添加到listview中
            musicList.Items.Add(lvi);
            // 结束listview的更新
            musicList.EndUpdate();
            // 将当前url存入urlList以便后续切换使用
            urlList.Add(url);
            // 存入musicSet方便去重
            musicSet.Add(url);
        }
    }
}

// 获取ogg文件的时长
private string getOggMusicDuration(string filePath)
{
    using (VorbisWaveReader reader = new VorbisWaveReader(filePath))
    {
    	// 获取时长
        TimeSpan duration = reader.TotalTime;
        // 得到分钟数
        int minutes = duration.Minutes;
        // 得到秒数
        int seconds = duration.Seconds;
        // 秒数保留两位数(格式化)
        string formattedSeconds = seconds.ToString("D2");
        // 返回格式化的时长
        return $"{minutes}:{formattedSeconds}";
    }
}

// 获取一般格式(如mp3、flac等)的时长
private string getCommonMusicDuration(string filePath)
{
    using (AudioFileReader reader = new AudioFileReader(filePath))
    {
        TimeSpan duration = reader.TotalTime;
        int minutes = duration.Minutes;
        int seconds = duration.Seconds;
        string formattedSeconds = seconds.ToString("D2");
        return $"{minutes}:{formattedSeconds}";
    }
}

导入歌曲的逻辑比较清晰,打开资源管理器——选择文件——去重——添加路径——更新列表。简单说明一下要注意的点:

  • 选择文件时,我并没有将ogg格式和其他格式分开考虑,而是进行了统一的处理,如果播放时选择的时一般格式,就会用wmp播放,如果是ogg格式,就会使用NAudio来播放
  • 去重这里直接选择了HashSet,每次遍历一个url就会到这个set里查询是否存在,如果已经存在则直接跳过,如果不存在,则在处理完后将其添加到set中
  • 添加路径到urlList后,实际上它的顺序是和listview中的歌曲顺序对应的,因为添加时就是一一对应来添加的,这样做的好处就是,在切换歌曲时直接通过urllist中的序号来对照listview中的序号,就可以直接通过url找到对应的歌曲了

2.4.2 选择播放歌曲(重点)

// 根据后缀名播放对应格式歌曲
private async Task playMusic(string filePath,int selectIndex) { 
	// 获取后缀名
    string ext = Path.GetExtension(filePath);
    // 设置播放状态
    UCHome.playbackState = 1;
    switch (ext) {
    	// 播放ogg
        case ".ogg":
            option = 1;
            await playOgg(filePath);
            break;
        // wmp播放一般格式
        default:
            option = 0;
            playCommon(selectIndex);
            break;
    }
}

// wmp播放一般格式音乐
private void playCommon(int selectIndex) {
	// 先确保停止当前播放的音乐
	Form1.wmp.Ctlcontrols.stop();
	// 等待100ms,消除残留(之前提过)
	Thread.Sleep(100);
	// 通过选择项序号获得url播放歌曲
	Form1.wmp.URL = urlList[selectIndex];
}

// 音乐停止事件
private void OnPlaybackStopped(object sender, StoppedEventArgs e) {
	// 如果是手动停止,则直接停止音乐
    if (UCHome.isStoppedManually)
    {
    	// 将变量恢复位默认状态
        UCHome.isStoppedManually = false;
        // 清空选中项
        musicList.SelectedIndices.Clear();
    }
    // 如果是自动停止,则需要播放下一首
    else
    {
    	// 播放下一首时也触发了停止事件,需要判断是不是手动播放的下一首
    	// 如果是手动点击的下一首
        if (UCHome.isNextManually)
        {
        	// 恢复默认状态
            UCHome.isNextManually = false;
            // 播放下一首的动作在手动点击下一首按钮时已经触发,这里不需要再写下一首的逻辑
        }
        else
        {
        	// 如果是自动播放下一首,这里需要调用下一首的逻辑
            UCHome.ucHome.clickNext();
        }
    }
}

// 播放ogg音乐
private async Task playOgg(string filePath) {
	// 通过NAudio和NAudio.Vorbis播放ogg
    using (VorbisWaveReader reader = new VorbisWaveReader(filePath))
    {
        using (WaveOutEvent outputDevice = new WaveOutEvent())
        {
        	// 添加歌曲停止事件,每次停止时触发
            outputDevice.PlaybackStopped += OnPlaybackStopped;
            // 初始化播放设备
            outputDevice.Init(reader);
            // 设置无限循环来监听播放状态的变化
            while (true) {
            	// 判断播放状态
                switch (UCHome.playbackState) {
                	// 如果是停止状态,则停止音乐,清空播放设备
                    case -1:
                        outputDevice.Stop();
                        outputDevice.Dispose();
                        reader.Dispose();
                        return;
                    // 如果是暂停状态,则暂停音乐并维持这个状态,直到再次发生变化
                    case 0:
                        outputDevice.Pause();
                        while (UCHome.playbackState == 0) { 
                            await Task.Delay(10);
                        }
                        break;
                    // 如果是播放状态,则播放音乐并维持这个状态,直到再次发生变化
                    case 1:
                        outputDevice.Play();
                        while (UCHome.playbackState == 1)
                        {
                            await Task.Delay(10);
                        }
                        break;
                }
            }
        }
    }
}

// listview选择项变化事件
private  async void musicList_SelectedIndexChanged(object sender, EventArgs e)
{
	// 如果选择项个数大于0则触发事件
    if (musicList.SelectedItems.Count>0) {
    	// 切换歌曲等效于手动下一首,所以设置isNextManually为true
        UCHome.isNextManually = true;
        // 停止当前播放的歌曲
        UCHome.ucHome.clickPicStop();
        // 等待足够时间至停止事件响应,确保消除残留
        await Task.Delay(100);
        // 停止事件按预想正常触发后恢复isNextManually的默认状态
        UCHome.isNextManually = false;

		// 可能多线程等待停止时间不匹配,会出现没有选择项但播放歌曲的情况,这时会出现异常
		// 此时选择项数量为0,可以再多等待一段时间,增加容错
        if (musicList.SelectedItems.Count == 0) {
            await Task.Delay(100);
            // 如果时间依然不匹配则放弃播放响应(概率较小)
            if(musicList.SelectedItems.Count == 0) return;
        }
        // 因为设置了listview只能单选,所以播放的歌曲就是第一个选中项
        // 获取第一个选中项的索引
        int selectIndex = musicList.SelectedItems[0].Index;
        // 跟踪当前播放歌曲的索引,以便上一首和下一首功能实现
        curIndex = selectIndex;
        // 获取对应歌曲的url
        string filePath = urlList[selectIndex].ToString();
		// 下面两行为样式变化,即在Home页面显示歌名,这里不作过多描述
        UCHome.ucHome.changeCurName(Path.GetFileNameWithoutExtension(filePath));
        UCHome.ucHome.changeStateText(true);
		// 等待播放线程
        await playMusic(filePath, selectIndex);
    }
}

播放歌曲第一个要明确的点是,整个播放流程都是在单独的一个线程中完成的。用wmp播放是自动就会在后台播放歌曲,但是手动播放ogg音乐需要挂起当前线程,等待播放结束,否则ogg音乐来不及播放,当前函数就结束了,也就导致音乐不能播放。之所以不使用Sleep函数,是因为Sleep会阻塞当前线程,导致主程序在音乐播放时无法操作。
第二点,也是本项目的一个亮点吧,就是我将一般格式和ogg格式整合起来了,通过识别后缀名来播放对应格式的歌曲,一般格式用wmp播放,ogg用NAudio播放,在使用时用户并不会注意到两者的差异,即完成了播放状态的统一。
第三点,算是一个缺点,ogg音乐的状态是通过全局变量playBackState来控制的,所以程序需要监听这个变量的状态来作出响应,我使用的监听策略是每隔一小段时间,就检查一下变量是否改变(即代码中的无限循环),没有变动就保持挂起状态,有变动就做出相应反应。这样做逻辑上可行,但并不完美,因为要使用户无感知就得保证检查变量的间隔时间足够短,这样就导致cpu需要频繁检查变量的状态,造成cpu的性能浪费。其实有更好的监听策略,也许可以启动一个事件来监听这个变量的变化,在这个变量变动的时候触发事件,这样就避免了反复检查变量,提高了cpu的性能。
其他方面基本在注释中都说明了,要需要可以按需参考,主要还是以上三点。

3 小结

本项目完成了一个音乐播放器的demo,但这依然是一个雏形,只是把大致的UI轮廓和主要功能完成了。还有很多可以完善的地方,比如音乐进度条,播放结束后的策略(如顺序播放、随机播放)等等。作为一次课程作业来说,我认为目前的进度足够了,但作为一个真正的项目来说,还有一段不小的距离。此demo有亮点,如较好的UI设计、交互体验、以及整合wmp和ogg的播放,但也有不足,如ogg音乐状态的监听、项目整体的架构设计等。
总的来说,这次作业是一次不错的练手项目,对我来说是一段小的开发经验吧。
因为项目中绝大多数问题bug都是我一点一点修复完善的,特别是对于全局变量的处理,比如是否手动停止,是否手动下一首这两个变量,并不是我一开始就能想到,而是代码到了关键时刻无法进展,开始思考解决策略,自然而然就设置了这两个变量。
再比如两个子界面之间的全局变量要相互调用时该如何处理,我一开始想到的是用static关键字,对于变量来说没什么问题,但是调用函数时就不能这样处理了,因为可能会涉及静态函数和非静态函数之间相互调用的问题,我开始认识到不能一味地用static,于是我想到可以在类中初始化一个该类的实例成员,再用this关键字给实例本身赋值,这样一来,无论是变量还是函数都可以设置为public,再通过这个实例成员来调用了。
除了逻辑的实现之外,对于UI的实现我其实也费了不少心思,模仿了不少主流软件的交互响应方式,这也是一个不错的体验,但这并不是重点,所以我也就没在这方面赘述。
——————😋完结撒花😋——————

附 项目 github 链接

https://github.com/hespecial/music-player

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
用c++/qt写的项目,可供自己学习,项目都经测试过,真实可靠,请放心使用。Qt支持 Windows、Linux/Unix、Mac OS X、Android、BlackBerry、QNX等多种平台,并为这些不同的平台提供了统一的开发环境。 面向对象 C++是完全面向对象的,这一点和Objective-c等在开发很相似。而Qt又是基于C++一种语言的扩展,大家都知道C++ 有快速、简易、面向对象等很多优点,所以Qt自然也继承者C++这些的优点。 Qt良好的封装机制使得Qt的模块化程度非常高,可重用性较好,对用户开发来货是非常方便的。Qt提供一种为signals/slots(信号和槽) 的安全类型来替代callback,使得各个元件之间的协同工作变得十分简单。 丰富的API Qt包括多达 250 个以上的 C++ 类,还提供基于模板的 collections, serialization, file, I/Odevice, directory management, date/time 类。甚至还包括正则表达式的处理功能。 支持 2D/3D 图形渲染,支持 OpenGL。 大量的开发文档。 XML支持 Webkit 引擎的集成,可以实现本地界面与Web内容的无缝集成, 但是真正使得 Qt 在自由软件界的众多 Widgets (如 Lesstif,Gtk,EZWGL,Xforms,fltk 等等)中脱颖而出的还是基于 Qt 的重量级软件 KDE。 信号和槽机制 Qt提供了信号和槽机制用于完成见面操作的响应,是完成任意两个Qt对象之通信机制。其中,信号会在某个特定情况或动作下被触动,槽是等同于接受并处理信号的函数。 为什么方法不是直接调用的。中间用到 Signal 和槽机制不是多此一举? 其实在我们生活也是一样,老板级别的好说话,老板给助理分派任务也好说话,但是助理给老板分任务,可想而知会有什么后果,在以前的统治阶层肯定不允许这样的事发生。所以在分层思想中,我们所调用的函数也是这样的,上层可以调用下层和同一层的函数,下层函数不可以调用上层函数,否则程序的层次性会被打破,导致结构错综复杂,难以维护和管理。 那么怎样才能做到向上管理呢,有任务分配给老板怎么办? 老板会设立一个机构,也就是一个函数,用无限循环来查询助理的状态,如果助理真的有事情,这个机构就把这消息拿到老板来处理。但是这种处理方式显得有些复杂,我们想要的简单明了的方式是,如果助理有事件发生,可以直接调用老板函数处理。 说了这么多其实就是想说,信号和槽的最大优势在于,它完善了程序分层的思想,可以在不改变程序的层次性的情况下,完成由下层到上层的调用。在下层发出一个 Signal,这时上层与其想关联的 Slot 函数就会响应。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值