播放 .wav 时的 C# 消息队列
我有一个程序,当单击按钮时同步播放三个 .wav 文件。问题是,即使我在单击处理程序的第一条指令上禁用按钮,如果在播放声音时单击,其他鼠标单击事件也会排队,然后在播放完成时执行。我怎样才能避免这种情况?
这是我的点击处理程序,playSnippet_P1,2,3 以不同的顺序播放三个音频文件:
void btnPlay_Click(object sender, EventArgs e)
{
btnPlay.Enabled = false;
this.Refresh();
//play a snippet for the current passage
switch (myProgram.condition)
{
case 1:
playSnippet_P1();
break;
case 2:
playSnippet_P2();
break;
case 3:
playSnippet_P3();
break;
default:
if (myProgram.debug)
{
MessageBox.Show("Error(frmPassage): Invalid condition set: " + myProgram.condition);
}
break;
}
//leave this phase once final passage is finished
if (snipsPlayed >= (myProgram.snipCount_1
+ myProgram.snipCount_2
+ myProgram.snipCount_3))
{
myProgram.phaseController.runNextPhase();
}
//reset the form to show text for next passage
if (snipsPlayed >= (getSnipCount(1) + getSnipCount(2)))
currentPassage = 2;
else if (snipsPlayed >= getSnipCount(1))
currentPassage = 1;
else
currentPassage = 0;
lblTitle.Text = "Passage " + randPOrder[currentPassage].ToString();
btnPlay.Enabled = true;
}
private void playSnippet_P1()
{
snipsPlayed++;
int cSnipCount = getCSnipCount();
int snipNum = (snipsPlayed % cSnipCount);
int subNum = getSubNum(snipsPlayed);
if (snipNum == 0)
snipNum = cSnipCount;
int rPassage = randPOrder[currentPassage];
//play "Next question is for..."
JE_SP.playSound(Application.StartupPath
+ "\\res\\audio\\next\\Next" + subNum.ToString()
+ ".wav", true);
//play snippet
JE_SP.playSound(Application.StartupPath
+ "\\res\\audio\\passages\\Snip" + rPassage.ToString()
+ "-" + snipNum.ToString()
+ ".wav", true);
//play question
JE_SP.playSound(Application.StartupPath
+ "\\res\\audio\\passages\\Q" + rPassage.ToString()
+ "-" + snipNum.ToString()
+ ".wav", true);
string[] writeMe =
{
rPassage.ToString(),
"\\res\\audio\\passages\\Snip" + rPassage.ToString()
+ "-" + snipNum.ToString(),
"\\res\\audio\\passages\\Q" + rPassage.ToString()
+ "-" + snipNum.ToString(),
subNum.ToString(),
myProgram.condition.ToString()
};
JE_Log.logData(writeMe, "\t", myProgram.groupFile);
}
I have a program which plays three .wav files synchronously when a button is clicked. The problem is that even if I disable the button on the first instruction of the click handler, additional mouse click events are queued and then executed when playback completes if the use clicks while sound is playing. How can I avoid this?
Here's my click handler, playSnippet_P1,2,3 play the three audio files in different orders:
void btnPlay_Click(object sender, EventArgs e)
{
btnPlay.Enabled = false;
this.Refresh();
//play a snippet for the current passage
switch (myProgram.condition)
{
case 1:
playSnippet_P1();
break;
case 2:
playSnippet_P2();
break;
case 3:
playSnippet_P3();
break;
default:
if (myProgram.debug)
{
MessageBox.Show("Error(frmPassage): Invalid condition set: " + myProgram.condition);
}
break;
}
//leave this phase once final passage is finished
if (snipsPlayed >= (myProgram.snipCount_1
+ myProgram.snipCount_2
+ myProgram.snipCount_3))
{
myProgram.phaseController.runNextPhase();
}
//reset the form to show text for next passage
if (snipsPlayed >= (getSnipCount(1) + getSnipCount(2)))
currentPassage = 2;
else if (snipsPlayed >= getSnipCount(1))
currentPassage = 1;
else
currentPassage = 0;
lblTitle.Text = "Passage " + randPOrder[currentPassage].ToString();
btnPlay.Enabled = true;
}
private void playSnippet_P1()
{
snipsPlayed++;
int cSnipCount = getCSnipCount();
int snipNum = (snipsPlayed % cSnipCount);
int subNum = getSubNum(snipsPlayed);
if (snipNum == 0)
snipNum = cSnipCount;
int rPassage = randPOrder[currentPassage];
//play "Next question is for..."
JE_SP.playSound(Application.StartupPath
+ "\\res\\audio\\next\\Next" + subNum.ToString()
+ ".wav", true);
//play snippet
JE_SP.playSound(Application.StartupPath
+ "\\res\\audio\\passages\\Snip" + rPassage.ToString()
+ "-" + snipNum.ToString()
+ ".wav", true);
//play question
JE_SP.playSound(Application.StartupPath
+ "\\res\\audio\\passages\\Q" + rPassage.ToString()
+ "-" + snipNum.ToString()
+ ".wav", true);
string[] writeMe =
{
rPassage.ToString(),
"\\res\\audio\\passages\\Snip" + rPassage.ToString()
+ "-" + snipNum.ToString(),
"\\res\\audio\\passages\\Q" + rPassage.ToString()
+ "-" + snipNum.ToString(),
subNum.ToString(),
myProgram.condition.ToString()
};
JE_Log.logData(writeMe, "\t", myProgram.groupFile);
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
此行为的更简单重现:
问题在于,UI 线程繁忙时记录的鼠标单击会进入消息队列并卡在那里,直到事件处理程序完成。发生这种情况时,该按钮已再次启用,允许 Click 事件再次运行。
解决这个问题很麻烦,并且需要付出在工作线程上运行代码的代价。一个可能的好修复方法是在重新启用按钮之前清除消息队列并删除所有鼠标消息,但这在 Winforms 中并不容易。代码实际上在那里,但它是内部的。一个真正务实的解决方案可能会给我带来麻烦,但安全有效:
A simpler repro of this behavior:
The problem is that the mouse clicks that are recorded while the UI thread is busy go into the message queue and are stuck there until the event handler completes. When that happens, the button is already enabled again, allowed the Click event to run again.
Fixing this is fugly and includes paying the price of running the code on a worker thread. A possible good fix is to purge the message queue and remove all the mouse messages before re-enabling the button but that's not easy in Winforms. The code is actually there but it is internal. A really pragmatic fix is one that might get me in trouble but is safe and effective :
这是 Windows 特意设计的一个功能。这与让您在繁忙的窗口中输入内容是一样的 - 一旦应用程序返回,您的文本就会出现。
为什么不禁用按钮并异步播放声音?
This is a deliberate feature of Windows. It's the same thing that lets you type into a window that's busy - your text appears once the application comes back.
Why not disable the button and play the sounds asynchronously?
我从您的描述中假设playsnippet() 会阻塞,直到片段播放完毕,因此这应该在单独的线程中完成(您可以使用BackgroundWorker 类)。这将避免你的 GUI 被卡住,并且还应该解决按钮点击问题。
当BackgroundWorker完成后,您可以重新启用该按钮,但请确保在GUI线程上执行此操作(使用Control.Invoke或Control.BeginInvoke)
祝您好运。
I'm assuming from your description that playsnippet() blocks until the snippet is done playing, so this should be done a separate thread (you can use a BackgroundWorker class). This will avoid your GUI from getting stuck and should also solve the button clicking issue.
When the BackgroundWorker has completed, you can re-enable the button, but make sure to do this on the GUI thread (with Control.Invoke or Control.BeginInvoke)
Good luck.
我想出的解决方案如下:
在鼠标单击处理程序中,禁用该按钮,但在播放完成时不要重新启用它。相反,以足够的时间间隔启动 Forms.Timer,以允许清除到达禁用按钮的消息。在第一个刻度上,重新启用按钮并停止计时器。
这会阻止其他单击事件执行任何操作,因为按钮仍会被禁用。当然,这个解决方案非常适合我的情况,我知道通常解决方案是在单独的线程上播放音频。
感谢您的所有建议!
编辑:Hans Passant 发布了一个更明智的解决方案 - 请参阅他关于在启用控件之前清除消息队列的建议。
The solution I came up with is as follows:
In the mouse click handler, disable the button but do no re-enable it when playback completes. Instead start a Forms.Timer with sufficient interval to allow the messages that arrive on the disabled button to be cleared. On the first tick, re-enable the button and stop the timer.
This prevented the additional click events from doing anything because the button would still be disabled. Of course, this solution is very specific to my situation, and I understand usually the solution would be to play audio on a separate thread.
Thanks for all you suggestions!!
EDIT: Hans Passant posted a more sensible solution - see his suggestion on purging the message queue before enabling the control.