Hiển thị Bảng điều khiển trong Ứng dụng Windows?


85

Có cách nào để hiển thị bảng điều khiển trong ứng dụng Windows không?

Tôi muốn làm một cái gì đó như thế này:

static class Program
{
    [STAThread]
    static void Main(string[] args) {
        bool consoleMode = Boolean.Parse(args[0]);

        if (consoleMode) {
            Console.WriteLine("consolemode started");
            // ...
        } else {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

Câu trả lời:


77

Những gì bạn muốn làm là không thể thực hiện một cách lành mạnh. Có một câu hỏi tương tự, vì vậy hãy xem câu trả lời .

Sau đó, cũng có một cách tiếp cận điên rồ (trang web xuống - bản sao lưu có sẵn tại đây. ) Được viết bởi Jeffrey Knight :

Câu hỏi: Làm cách nào để tạo ứng dụng có thể chạy ở chế độ GUI (cửa sổ) hoặc chế độ dòng lệnh / bảng điều khiển?

Nhìn bề ngoài, điều này có vẻ dễ dàng: bạn tạo một ứng dụng Console, thêm biểu mẫu cửa sổ vào nó, và bạn đang tắt và chạy. Tuy nhiên, có một vấn đề:

Vấn đề: Nếu bạn chạy ở chế độ GUI, bạn sẽ có cả một cửa sổ và một bảng điều khiển khó chịu ẩn trong nền và bạn không có cách nào để ẩn nó.

Những gì mọi người dường như muốn là một ứng dụng lưỡng cư thực sự có thể chạy trơn tru ở cả hai chế độ.

Nếu bạn chia nhỏ nó, thực sự có bốn trường hợp sử dụng ở đây:

User starts application from existing cmd window, and runs in GUI mode
User double clicks to start application, and runs in GUI mode
User starts application from existing cmd window, and runs in command mode
User double clicks to start application, and runs in command mode.

Tôi đang đăng mã để thực hiện việc này, nhưng có một lời cảnh báo.

Tôi thực sự nghĩ rằng kiểu tiếp cận này sẽ khiến bạn gặp nhiều rắc rối hơn mức đáng có. Ví dụ: bạn sẽ phải có hai giao diện người dùng khác nhau - một cho GUI và một cho lệnh / shell. Bạn sẽ phải xây dựng một công cụ logic trung tâm kỳ lạ nào đó tóm tắt từ GUI so với dòng lệnh và nó sẽ trở nên kỳ lạ. Nếu là tôi, tôi sẽ lùi lại và suy nghĩ về cách điều này sẽ được sử dụng trong thực tế, và liệu kiểu chuyển đổi chế độ này có đáng để làm việc hay không. Do đó, trừ khi một số trường hợp đặc biệt được yêu cầu, tôi sẽ không tự mình sử dụng mã này, bởi vì ngay khi tôi gặp phải các tình huống mà tôi cần lệnh gọi API để hoàn thành công việc, tôi có xu hướng dừng lại và tự hỏi bản thân "tôi có đang phức tạp hóa mọi thứ không? ".

Loại đầu ra = Ứng dụng Windows

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Diagnostics;
using Microsoft.Win32;

namespace WindowsApplication
{
    static class Program
    {
        /*
    DEMO CODE ONLY: In general, this approach calls for re-thinking 
    your architecture!
    There are 4 possible ways this can run:
    1) User starts application from existing cmd window, and runs in GUI mode
    2) User double clicks to start application, and runs in GUI mode
    3) User starts applicaiton from existing cmd window, and runs in command mode
    4) User double clicks to start application, and runs in command mode.

    To run in console mode, start a cmd shell and enter:
        c:\path\to\Debug\dir\WindowsApplication.exe console
        To run in gui mode,  EITHER just double click the exe, OR start it from the cmd prompt with:
        c:\path\to\Debug\dir\WindowsApplication.exe (or pass the "gui" argument).
        To start in command mode from a double click, change the default below to "console".
    In practice, I'm not even sure how the console vs gui mode distinction would be made from a
    double click...
        string mode = args.Length > 0 ? args[0] : "console"; //default to console
    */

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool AllocConsole();

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool FreeConsole();

        [DllImport("kernel32", SetLastError = true)]
        static extern bool AttachConsole(int dwProcessId);

        [DllImport("user32.dll")]
        static extern IntPtr GetForegroundWindow();

        [DllImport("user32.dll", SetLastError = true)]
        static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

        [STAThread]
        static void Main(string[] args)
        {
            //TODO: better handling of command args, (handle help (--help /?) etc.)
            string mode = args.Length > 0 ? args[0] : "gui"; //default to gui

            if (mode == "gui")
            {
                MessageBox.Show("Welcome to GUI mode");

                Application.EnableVisualStyles();

                Application.SetCompatibleTextRenderingDefault(false);

                Application.Run(new Form1());
            }
            else if (mode == "console")
            {

                //Get a pointer to the forground window.  The idea here is that
                //IF the user is starting our application from an existing console
                //shell, that shell will be the uppermost window.  We'll get it
                //and attach to it
                IntPtr ptr = GetForegroundWindow();

                int  u;

                GetWindowThreadProcessId(ptr, out u);

                Process process = Process.GetProcessById(u);

                if (process.ProcessName == "cmd" )    //Is the uppermost window a cmd process?
                {
                    AttachConsole(process.Id);

                    //we have a console to attach to ..
                    Console.WriteLine("hello. It looks like you started me from an existing console.");
                }
                else
                {
                    //no console AND we're in console mode ... create a new console.

                    AllocConsole();

                    Console.WriteLine(@"hello. It looks like you double clicked me to start
                   AND you want console mode.  Here's a new console.");
                    Console.WriteLine("press any key to continue ...");
                    Console.ReadLine();       
                }

                FreeConsole();
            }
        }
    }
}

13
Tôi thấy thật mỉa mai với Microsoft và cách họ muốn tạo giao diện C # cho tất cả API của nó, nhưng không có cách nào trong C # để thực hiện một tác vụ đơn giản như vậy.
Ramon Zarazua B.

3
Thay vì phụ thuộc vào bảng điều khiển là cửa sổ nền trước, bạn có thể nhận được id quy trình mẹ của quy trình hiện tại bằng cách sử dụng winapi: stackoverflow.com/a/3346055/855432
ghord 11/09/12

2
Tính đến thời điểm viết bài, bản sao lưu của bài báo có sẵn tại đây web.archive.org/web/20111227234507/http://www.THERilver.com/…
Andrew Savinykh

2
Chào! Tôi thấy rằng nếu tôi chạy giải pháp này từ shell như Far, nc nó sẽ tạo ra bảng điều khiển mới. Nếu tôi đính kèm với Far Console như cmd, nó hoạt động sai. Tôi khuyên bạn nên tạo ConsoleApplication và nếu cần GUI thì hãy làm FreeConsole (); Bài báo xuất sắc! Cảm ơn!
Maxim Vasiliev

6
Tôi khuyên bạn nên gọi AttachConsolebằng -1(giá trị của hằng số API ATTACH_PARENT_PROCESS) thay vì hy vọng cửa sổ nền trước là cửa sổ lệnh phù hợp để ghi vào.
Jon Hanna

70

Điều này đã cũ một chút (OK, nó RẤT cũ), nhưng tôi đang làm điều tương tự ngay bây giờ. Đây là một giải pháp rất đơn giản phù hợp với tôi:

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AllocConsole();

[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();

[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

const int SW_HIDE = 0;
const int SW_SHOW = 5;

public static void ShowConsoleWindow()
{
    var handle = GetConsoleWindow();

    if (handle == IntPtr.Zero)
    {
        AllocConsole();
    }
    else
    {
        ShowWindow(handle, SW_SHOW);
    }
}

public static void HideConsoleWindow()
{
    var handle = GetConsoleWindow();
    ShowWindow(handle, SW_HIDE);
}

5
nếu bạn đang chạy điều này trong cửa sổ cmd, điều này sẽ mở ra một cửa sổ giao diện điều khiển khác mà không mong muốn trong quá trình tự động sẽ cần nắm bắt đầu ra giao diện điều khiển.
AaA

Cuối cùng, tôi đã sử dụng mã được cung cấp nhưng đã thêm một hàm chia sẻ InitConsole () để thực hiện phần AllocConsole () nếu xử lý là intptr.zero. Khi tôi sử dụng ShowConsoleWindow sau đó in trên nó ngay sau đó, nó không hoạt động. Phân bổ bảng điều khiển khi ứng dụng khởi động sau đó sử dụng ShowConsoleWindow đã hoạt động. Ngoài ra, điều này là hoàn hảo đối với tôi. Cảm ơn bạn ..
Sage Pourpre

Console.WriteLinenhật ký không hiển thị trong trường hợp bắt đầu từ cmd với args
Mohammad Ali

Đừng quênusing System.Runtime.InteropServices;
Darren Griffith

19

Cách dễ nhất là khởi động ứng dụng WinForms, đi tới cài đặt và thay đổi loại thành ứng dụng bảng điều khiển.


1
Ứng dụng -> loại đầu ra: Ứng dụng Console. Đã làm điều đó cho tôi!
Lodewijk

Điều đó làm việc tuyệt vời. Hy vọng rằng nó không làm rối tung bất cứ điều gì khác. Cảm ơn và +1.
deathismyfriend

1
Khởi động ứng dụng bằng cách nhấp đúp vào nó sẽ mở ra một cửa sổ cmd
Mohammad Ali

13

Khước từ

Có một cách để đạt được điều này khá đơn giản, nhưng tôi sẽ không gợi ý rằng đó là một cách tiếp cận tốt cho một ứng dụng mà bạn sẽ cho người khác xem. Nhưng nếu bạn có một số nhà phát triển cần hiển thị bảng điều khiển và các biểu mẫu cửa sổ cùng một lúc, nó có thể được thực hiện khá dễ dàng.

Phương pháp này cũng hỗ trợ chỉ hiển thị cửa sổ Console, nhưng không hỗ trợ chỉ hiển thị Windows Form - tức là Console sẽ luôn được hiển thị. Bạn chỉ có thể tương tác (tức là nhận dữ liệu - Console.ReadLine(), Console.Read()) với cửa sổ giao diện điều khiển nếu bạn không hiển thị các biểu mẫu cửa sổ; xuất ra Console -Console.WriteLine() - hoạt động ở cả hai chế độ.

Điều này được cung cấp nguyên trạng; không đảm bảo điều này sẽ không làm điều gì đó kinh khủng sau này, nhưng nó hoạt động.

Các bước dự án

Bắt đầu từ một ứng dụng bảng điều khiển tiêu chuẩn .

Đánh dấu Mainphương pháp là[STAThread]

Thêm một tham chiếu trong dự án của bạn vào System.Windows.Forms

Thêm Biểu mẫu Windows vào dự án của bạn.

Thêm mã khởi động Windows chuẩn vào Mainphương pháp của bạn :

Kết quả cuối cùng

Bạn sẽ có một ứng dụng hiển thị Bảng điều khiển và các biểu mẫu cửa sổ tùy chọn.

Mã mẫu

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ConsoleApplication9 {
    class Program {

        [STAThread]
        static void Main(string[] args) {

            if (args.Length > 0 && args[0] == "console") {
                Console.WriteLine("Hello world!");
                Console.ReadLine();
            }
            else {
                Application.EnableVisualStyles(); 
                Application.SetCompatibleTextRenderingDefault(false); 
                Application.Run(new Form1());
            }
        }
    }
}

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ConsoleApplication9 {
    public partial class Form1 : Form {
        public Form1() {
            InitializeComponent();
        }

        private void Form1_Click(object sender, EventArgs e) {
            Console.WriteLine("Clicked");
        }
    }
}

mã đẹp ở đây. nhưng làm thế nào để bạn tắt hiển thị bảng điều khiển nếu bạn muốn bán hoặc triển khai hoặc thứ gì khác. ???
r4ccoon 20/09/09

@ r4ccoon - bạn không thể. Nhưng bạn có thể dễ dàng chuyển tất cả mã của mình sang một ứng dụng Windows bình thường.
Sam Meldrum

Vâng, IMHO có một cách đơn giản hơn để đạt được hiệu quả tương tự là tạo Dự án Windows Forms như bình thường, sau đó nhấp chuột phải vào nó trong Trình khám phá giải pháp -> Thuộc tính và thay đổi Loại đầu ra thành Ứng dụng bảng điều khiển. (chỉnh sửa: bây giờ tôi đã nhận ra đó là về cơ bản ICR của câu trả lời)
kamilk

Đẹp Từng bước trả lời
AminM

9

Khôi phục một chủ đề cũ một lần nữa, vì không có câu trả lời nào ở đây phù hợp với tôi.

Tôi đã tìm thấy một cách đơn giản có vẻ khá mạnh mẽ và đơn giản. Nó đã làm việc cho tôi. Ý tưởng:

  • Biên dịch dự án của bạn dưới dạng Ứng dụng Windows. Có thể có bảng điều khiển dành cho cha mẹ khi tệp thực thi của bạn bắt đầu, nhưng có thể không. Mục đích là sử dụng lại bảng điều khiển hiện có nếu có hoặc tạo một bảng điều khiển mới nếu không.
  • AttachConsole (-1) sẽ tìm kiếm giao diện điều khiển của tiến trình mẹ. Nếu có, nó sẽ gắn vào nó và bạn đã hoàn thành. (Tôi đã thử điều này và nó hoạt động bình thường khi gọi ứng dụng của tôi từ cmd)
  • Nếu AttachConsole trả về false, không có bảng điều khiển mẹ. Tạo một cái bằng AllocConsole.

Thí dụ:

static class Program
{
    [DllImport( "kernel32.dll", SetLastError = true )]
    static extern bool AllocConsole();

    [DllImport( "kernel32", SetLastError = true )]
    static extern bool AttachConsole( int dwProcessId );

    static void Main(string[] args)
    {
        bool consoleMode = Boolean.Parse(args[0]);
        if (consoleMode)
        {
           if (!AttachConsole(-1))
              AllocConsole();
           Console.WriteLine("consolemode started");
           // ...
        } 
        else
        {
           Application.EnableVisualStyles();
           Application.SetCompatibleTextRenderingDefault(false);
           Application.Run(new Form1());
        }
    }
}

Một lời cảnh báo: có vẻ như nếu bạn cố gắng ghi vào bảng điều khiển trước khi gắn hoặc cấp phát bảng điều khiển, thì phương pháp này không hoạt động. Tôi đoán là lần đầu tiên bạn gọi Console.Write / WriteLine, nếu chưa có bảng điều khiển thì Windows sẽ tự động tạo một bảng điều khiển ẩn ở đâu đó cho bạn. (Vì vậy, có lẽ câu trả lời ShowConsoleWindow của Anthony sẽ tốt hơn sau khi bạn đã viết vào bảng điều khiển và câu trả lời của tôi sẽ tốt hơn nếu bạn chưa viết vào bảng điều khiển). Điều quan trọng cần lưu ý là điều này không hoạt động:

static void Main(string[] args)
    {
        Console.WriteLine("Welcome to the program");   //< this ruins everything
        bool consoleMode = Boolean.Parse(args[0]);
        if (consoleMode)
        {
           if (!AttachConsole(-1))
              AllocConsole();
           Console.WriteLine("consolemode started");   //< this doesn't get displayed on the parent console
           // ...
        } 
        else
        {
           Application.EnableVisualStyles();
           Application.SetCompatibleTextRenderingDefault(false);
           Application.Run(new Form1());
        }
    }

cảm ơn bạn đã chia sẻ mã mẫu. Tôi đã thử và thấy rằng nó hoạt động nhưng có những hạn chế. Không có chuyển hướng đầu ra bảng điều khiển (có thể được sửa bằng mã nguồn bổ sung). Nhưng nhược điểm chính là nó trả lại quyền điều khiển ngay lập tức cho bàn điều khiển. Ví dụ khi tôi gõ \bin\Debug>shareCheck.exe /once và nhấn Enter command prompt được hiển thị và sau đó an ủi bắt đầu đến đầu ra: \bin\Debug>hello. It looks like you started me from an existing console.và khi chương trình kết thúc không có command prompt dòng đầu ra nên cuối cùng và màn hình trống đó là một chút cắn điên
oleksa

Cảm ơn Kevin vì lời cảnh báo - Tôi đang gặp sự cố với các cách tiếp cận được đề xuất trong bài đăng SO này và dường như tôi đang nhận được "bảng điều khiển ẩn" mặc dù tôi không có bất kỳ đầu ra Console nào xảy ra trước đó ... Hóa ra là cửa sổ "Đầu ra" trong Visual Studio là bảng điều khiển ẩn, nếu ứng dụng của bạn đang chạy với Trình gỡ lỗi được đính kèm! Đáng nói ... (vì vậy, chương trình của tôi đã hoạt động khi chuyển sang chạy mà không có trình gỡ lỗi, tức là Ctrl-F5)
Per Lundberg

3

Điều hiệu quả đối với tôi là viết riêng một ứng dụng console làm những gì tôi muốn nó làm, biên dịch nó thành exe, và sau đó làm Process.Start("MyConsoleapp.exe","Arguments")


1
Đó là phiên bản dao cạo của Occam. Không thách thức đủ: P
Michael Hoffmann

3

Kiểm tra mã nguồn này. Tất cả mã nhận xét - được sử dụng để tạo bảng điều khiển trong ứng dụng Windows. Bỏ chú thích - để ẩn bảng điều khiển trong ứng dụng bảng điều khiển. Từ đây . (Trước đây ở đây .) Dự án reg2run.

// Copyright (C) 2005-2015 Alexander Batishchev (abatishchev at gmail.com)

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace Reg2Run
{
    static class ManualConsole
    {
        #region DllImport
        /*
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool AllocConsole();
        */

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr handle);

        /*
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        private static extern IntPtr CreateFile([MarshalAs(UnmanagedType.LPStr)]string fileName, [MarshalAs(UnmanagedType.I4)]int desiredAccess, [MarshalAs(UnmanagedType.I4)]int shareMode, IntPtr securityAttributes, [MarshalAs(UnmanagedType.I4)]int creationDisposition, [MarshalAs(UnmanagedType.I4)]int flagsAndAttributes, IntPtr templateFile);
        */

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool FreeConsole();

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        private static extern IntPtr GetStdHandle([MarshalAs(UnmanagedType.I4)]int nStdHandle);

        /*
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetStdHandle(int nStdHandle, IntPtr handle);
        */
        #endregion

        #region Methods
        /*
        public static void Create()
        {
            var ptr = GetStdHandle(-11);
            if (!AllocConsole())
            {
                throw new Win32Exception("AllocConsole");
            }
            ptr = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, 3, 0, IntPtr.Zero);
            if (!SetStdHandle(-11, ptr))
            {
                throw new Win32Exception("SetStdHandle");
            }
            var newOut = new StreamWriter(Console.OpenStandardOutput());
            newOut.AutoFlush = true;
            Console.SetOut(newOut);
            Console.SetError(newOut);
        }
        */

        public static void Hide()
        {
            var ptr = GetStdHandle(-11);
            if (!CloseHandle(ptr))
            {
                throw new Win32Exception();
            }
            ptr = IntPtr.Zero;
            if (!FreeConsole())
            {
                throw new Win32Exception();
            }
        }
        #endregion
    }
}

1

Trên thực tế, AllocConsole với SetStdHandle trong ứng dụng GUI có thể là một cách tiếp cận an toàn hơn. Vấn đề với việc "chiếm quyền điều khiển" đã được đề cập, là giao diện điều khiển có thể không phải là một cửa sổ nền trước, (đặc biệt là xem xét dòng chảy của các trình quản lý cửa sổ mới trong Vista / Windows 7) trong số những thứ khác.


0

Trong wind32, các ứng dụng chế độ giao diện điều khiển là một con thú hoàn toàn khác với các ứng dụng nhận hàng đợi tin nhắn thông thường. Chúng được khai báo và biên dịch khác nhau. Bạn có thể tạo một ứng dụng có cả phần bảng điều khiển và cửa sổ bình thường và ẩn cái này hoặc cái kia. Nhưng nghi ngờ rằng bạn sẽ thấy toàn bộ mọi thứ hoạt động hơn bạn tưởng một chút.


Wind32 nghe có vẻ giống như một thứ gì đó thuộc về một diễn đàn khí tượng học nhưng nếu bạn muốn nói đến Win32 thì hãy lưu ý rằng chúng ta có thể tạo vòng lặp thông báo trong các chương trình console, như trong PostThreadMessage to Console Application .
user34660

0

Theo trích dẫn của Jeffrey Knight ở trên, ngay khi tôi gặp phải các tình huống mà tôi cần lệnh gọi API để hoàn thành công việc, tôi có xu hướng dừng lại và tự hỏi bản thân "liệu mình có đang phức tạp hóa mọi thứ không?".

Nếu điều bạn muốn là có một số mã và chạy nó ở chế độ Windows GUI hoặc chế độ Bảng điều khiển, hãy cân nhắc chuyển mã được sử dụng trong cả hai chế độ sang thư viện mã DLL, sau đó có ứng dụng Windows Forms sử dụng DLL đó và Bảng điều khiển ứng dụng sử dụng DLL đó (tức là nếu trong Visual Studio bây giờ bạn có giải pháp ba dự án: thư viện với phần lớn mã, GUI chỉ với mã Win Forms và Bảng điều khiển chỉ với mã bảng điều khiển của bạn.)


0

Và một câu trả lời muộn màng khác. Tôi không thể nhận được bất kỳ đầu ra nào cho bảng điều khiển được tạo AllocConsoletheo các đề xuất trước đó, vì vậy thay vào đó tôi đang bắt đầu với ứng dụng Bảng điều khiển . Sau đó, nếu bảng điều khiển không cần thiết:

        [DllImport("kernel32.dll")]
        private static extern IntPtr GetConsoleWindow();

        [DllImport("user32.dll")]
        private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        private const int SW_HIDE = 0;
        private const int SW_SHOW = 5;

        [DllImport("user32.dll", SetLastError = true)]
        static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

        public static bool HideConsole()
        {
            var hwnd = GetConsoleWindow();
            GetWindowThreadProcessId(hwnd, out var pid);

            if (pid != Process.GetCurrentProcess().Id) // It's not our console - don't mess with it.
                return false;

            ShowWindow(hwnd, SW_HIDE);

            return true;
        }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.