模拟窗体中的阻塞控制台键盘输入



我需要用C#编写一个控制台模拟器。我想用可以在全屏工作的东西来代替System.Console。我的方法是使一个最大化的形式没有边界。

我面临的一个挑战是如何将事件驱动的非阻塞键盘输入转换为阻塞键盘输入,或者更具体地说,如何在Windows窗体中实现等效的Console.ReadKey()

我不认为您真的想要拥有完全阻塞的控制台窗口。您需要的更像PowerShell或标准控制台窗口。我的意思是,这是值得怀疑的,你将反对有能力移动窗口,从控制台复制数据等等


因此,进一步描述的控制台是基于TextBox的。为什么不在Form本身上?-因为多行、只读的TextBox已经提供了许多简化ConsoleAppendText、文本选择、复制…)的功能

1.理念和接口:

正如您在问题中已经提到的,WinForms具有与控制台应用程序完全不同的模型,具有消息队列和基于事件的处理。所以,为了从事件中获取数据并防止表单变得不负责任,我们不能阻止这个事件循环。为了实现这种行为,我们将使用Task.Run在不同的线程上运行基于控制台的代码,并从主窗体线程中的事件中取消阻止依赖输入的"控制台"调用。应用程序的基石将是接下来的两个接口:

public interface IConsole
{
    ConsoleKeyInfo ReadKey();
    void WriteLine(String line);
}
public interface IConsoleContext
{
    void Run(Action<IConsole> main);
}

首先是控制台本身,它具有反映标准控制台功能的方法。第二个将在另一个线程上运行与控制台相关的代码(由操作表示),为该操作提供一些控制台对象。

我们的IConsoleContext实现将是TextBoxConsoleContext,其私有嵌套类TextBoxConsole将作为IConsole

2.主要表单代码

以下是我们将在表单中用于演示控制台的代码。假设我们有文本框textBox_Console:

private void Form1_Load(object sender, EventArgs e)
{
    var consoleContext = new TextBoxConsoleContext(this.textBox_Console);
    consoleContext.Run((console) =>
    {
        console.WriteLine("Welcome to the TextBox console");
        console.WriteLine("Press any key:");
        console.ReadKey();
        ConsoleKeyInfo keyInfo = new ConsoleKeyInfo();

        console.WriteLine("Press y to continue: ");
        do
        {
            keyInfo = console.ReadKey();
            if (keyInfo.KeyChar == 'y')
                break;
            console.WriteLine("You have entered another key, please enter y to continue:");
        }
        while (true);
        console.WriteLine("Thank you for your cooperation.");
    });
}

3.TextBoxConsoleContext本身:

public class TextBoxConsoleContext : IConsoleContext
{
    #region Nested types
    private class TextBoxConsole : IConsole
    {
        #region Fields
        private TextBoxConsoleContext parent;
        #endregion

        #region Constructors
        public TextBoxConsole(TextBoxConsoleContext parent)
        {
            if (parent == null)
                throw new ArgumentNullException("parent");
            this.parent = parent;
        }
        #endregion

        #region IConsole implementation
        public ConsoleKeyInfo ReadKey()
        {
            var key = this.parent.m_Queue.Dequeue();
            this.WriteLine(key.KeyChar.ToString());
            return key;
        }
        public void WriteLine(string line)
        {
            Action writeLine = () =>
                {
                    var textToAppend = String.Format("{0}{1}",
                        line,
                        Environment.NewLine);
                    this.parent.m_TextBox.AppendText(textToAppend);
                };
            this.parent.m_TextBox.Invoke(writeLine);
        }
        #endregion
    }
    #endregion

    #region Fields
    private TextBox m_TextBox;
    private OnRequestProducerConsumerQueue<ConsoleKeyInfo> m_Queue = new OnRequestProducerConsumerQueue<ConsoleKeyInfo>();
    private Boolean m_Shift;
    private Boolean m_Alt;
    private Boolean m_Ctrl;
    private ConsoleKey m_KeyInfo;
    #endregion

    #region Constructors
    public TextBoxConsoleContext(TextBox textBox)
    {
        if (textBox == null)
            throw new ArgumentNullException("textBox");

        this.m_TextBox = textBox;
        this.m_TextBox.ReadOnly = true;
        // Event handler that will read key down data before key press
        this.m_TextBox.KeyDown += (obj, e) =>
            {
                this.m_Shift = e.Modifiers.HasFlag(Keys.Shift);
                this.m_Alt = e.Modifiers.HasFlag(Keys.Alt);
                this.m_Ctrl = e.Modifiers.HasFlag(Keys.Control);
                if (!Enum.TryParse<ConsoleKey>(e.KeyCode.ToString(), out this.m_KeyInfo))
                {
                    this.m_KeyInfo = ConsoleKey.Escape;
                }                   
            };
        this.m_TextBox.KeyPress += (obj, e) =>
            {
                this.m_Queue.EnqueueIfRequired(new ConsoleKeyInfo(e.KeyChar, 
                    this.m_KeyInfo, 
                    this.m_Shift, 
                    this.m_Alt,
                    this.m_Ctrl));
            };
    }
    #endregion

    #region IConsoleContext implementation
    public void Run(Action<IConsole> main)
    {
        if (main == null)
            throw new ArgumentNullException("main");
        var console = new TextBoxConsole(this);
        Task.Run(() =>
            main(console));
    }
    #endregion
}

4.OnRequestProducerConsumerQueue

EDIT:我用这个简单得多的类替换了初始队列类,该类使用带有Monitor调用的单个锁定对象。此代码基于条件变量模式。

备注:我只是想替换我最初发布的滥用同步结构的内容。这个版本不会改变任何行为,如果您需要实现ReadLine方法,那么这个实现中的EnqueueIfRequired将非常无用(但您可以添加一些谓词逻辑,允许在出现行终止符之前进行queening)。

public class OnRequestProducerConsumerQueue<T>
{
    private Queue<T> m_Items = new Queue<T>();
    private object m_lock = new Object();
    private Int32 m_NeedItems = 0;
    public void EnqueueIfRequired(T value)
    {
        lock (this.m_lock)
        {
            if (this.m_NeedItems == 0)
                return;
            this.m_Items.Enqueue(value);
            this.m_NeedItems--;
            Monitor.PulseAll(this.m_lock);
        }
    }
    public T Dequeue()
    {
        lock (this.m_lock)
        {
            this.m_NeedItems++;
            while (this.m_Items.Count < 1)
            {
                Monitor.Wait(this.m_lock);
            }
            return this.m_Items.Dequeue();
        }
    }
}

最新更新