使用云存储服务来进行终端之间的数据交换

的认证是通过 OAuth 2.0 协议的,这是一项开放的认证协议,第三方应用程序和网站可以对认证用户保护的资源进行有限的访问而不需要传输密码信息。基本的 OAuth 2.0 访问场景包含4步,

  1. 首先,您需要取得用于认证的数据 (客户的 ID 和密码),这些数据是由网站生成的,并且网站和应用程序都是知道的。
  2. 在应用程序能够访问个人数据之前,它应当收到访问令牌,一个这样的令牌可以提供由'scope'变量定义的不同级别的访问。当请求访问令牌时,应用程序可以在'scope'参数中发送一个或多个值,为了创建这个请求,应用程序可以使用系统浏览器和 web service 请求。有些请求需要在用户登录他们账户的时候有一个认证步骤,在登录之后,用户被询问,他们是否准备给应用程序请求以许可,这个过程被称为用户同意(user consent)。如果用户提出同意,认证服务器就给应用程序提供认证编码,使得应用程序可以获得访问令牌,如果用户没有同意,服务器就返回错误。
  3. 在应用程序收到访问令牌之后,它会以HTTP认证头的形式发送它,访问点只有在请求的'scope'参数中有描述的操作和资源时才是有效的,例如,如果许可了 Google Drive的访问令牌,它不会提供对 Google contacts(谷歌联系人)的访问,但是,应用程序可以对Google Drive多次发送访问令牌以进行允许的操作。
  4. 令牌的生命周期是有限的,如果应用程序在访问令牌过期后还需要访问,它可以取得一个更新令牌来允许应用程序取得新的访问令牌。

2. 准备访问 Google Drive

为应用程序创建一个新的项目。转到项目面板 ("选择一个项目"按钮或者 Ctrl + O). 创建一个新的项目 (+).

在新打开的页面,设置项目名称,同意使用声明并确认创建。

从面板中选择新项目并把它与Google Drive API关联,为此, 在管理器 API 开发库中选择 Drive API,并且在新的页面点击Enable(启用)来激活它。

新的页面提示我们创建信任以使用 API. 点击 "Create credentials(创建信任)" 来这样做。

Google 控制台提供了向导来选择认证类型,但是我们不需要它。点击 "client ID". 下一步,Google 再次警告我们需要配置访问确认页面,点击 "Configure consent screen(配置同意屏幕)" 来这样做。

在新打开的页面中,只填充 "Product name shown to users(向用户显示的产品名称)",其它栏位都保持默认值。下一步,把应用程序类型设为"Other(其它)", 指定客户名称并点击"Create(创建)"。服务会生成 "client ID" 和 "client secret" 代码,您可以复制它们,但这不是必须的:您可以以一个json文件的形式下载它们。点击 "Ok" 并下载用于访问本地磁盘数据的 json 文件,

在那之后,我们在服务端的准备工作就完成了,可以开发我们的应用程序了。

3. 在本地应用程序和 Google Drive 之间创建桥梁

为了解决这个任务,我已经开发了一个独立的程序(一种桥梁),它可以从 MetaTrader EA 或者指标中接收请求和数据,处理它们,再与 Google Drive 交互并把数据返回给 MetaTrader 应用程序。使用这种方法的优点,首先是 Google 提供了开发库用于使用 C# 操作 Google Drive,这使开发更加方便。其次,使用第三方应用程序来防止终端进行因外部服务带来的“资源消耗严重”的交易操作。第三,这可以使我们的应用程序不必与平台绑定,并且可以使它具有跨平台的能力,可以同时用于 MetaTrader 4 和 MetaTrader 5 应用程序。

就像我之前说过的,桥梁应用程序将会使用 Google 开发库,以 C# 语言开发。让我们在 VisualStudio 中创建 Windows Form 项目,并且使用 NuGet 加上 Google.Apis.Drive.v3 开发库。

下一步,让我们创建 GoogleDriveClass 类用于操作 Google Drive:

class GoogleDriveClass
    {
        static string[] Scopes = { DriveService.Scope.DriveFile };  //用于操作类的数组
        static string ApplicationName = "Google Drive Bridge";      //应用程序名称
        public static UserCredential credential = null;             //认证密钥
        public static string extension = ".gdb";                    //所保存文件的扩展名
    }

首先,让我们创建函数用于登录到服务,它将会应用之前保存的含有访问代码的 json 文件,在我的例子中,它是 "client-secret.json"。如果您已经使用不同的名称保存了文件,就在函数代码中设定它。在载入保存的数据之后,就调用服务中的异步认证函数,如果成功登录, 就在 credential 对象中保存 token 用于后来的访问。 当使用 C# 开发时,不要忘记处理异常: 如果有异常发生, credential 对象会被重置。 

        public bool Authorize()
        {
            using (System.IO.FileStream stream =
                     new System.IO.FileStream("client-secret.json", System.IO.FileMode.Open, System.IO.FileAccess.Read))
            {
                try
                {
                    string credPath = System.Environment.CurrentDirectory.ToString();
                    credPath = System.IO.Path.Combine(credPath, "drive-bridge.json");

                    credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                        GoogleClientSecrets.Load(stream).Secrets,
                        GoogleDriveClass.Scopes,
                        "example.bridge@gmail.com",
                        CancellationToken.None,
                        new FileDataStore(credPath, true)).Result;
                }
                catch (Exception)
                {
                    credential = null;
                }

            }
            return (credential != null);
        }

当操作 Google Drive 时, 我们的 "桥梁" 应当执行两个功能: 把数据写到磁盘中以及从所需文件中读取它。让我们更加详细地讨论它们。为了实现这样看起来简单的功能,我们还是需要写多个过程,原因是 Google Drive 的文件系统和我们熟悉的不同,在这里,文件的名称和扩展名是各自独立的记录,只是用于保持显示功能,实际上,当保存的时候,在存储中每个文件都被赋予唯一的ID,这样,用户可以使用相同的名称和扩展名保存不限数量的文件,所以,在访问文件之前,我们需要知道它在云存储中的ID。为此,要载入盘中所有文件的列表并挨个把它们的名称与指定名称作比较。

GetFileList 函数就是负责取得文件列表的,它会返回 Google.Apis.Drive.v3.Data.File 类的列表。让我们使用之前下载的开发库中的 Google.Apis.Drive.v3.DriveService 类来从 Google Drive 中取得文件列表,当初始化类的时候,我们向它传入登录时取得的令牌以及我们项目的名称,结果的列表保存在返回的 result 变量中。如果有异常发生,该变量会重置为0。如有必要,我们应用程序中的其它函数会请求和处理该文件列表。

using File = Google.Apis.Drive.v3.Data.File;
        public IList<File> GetFileList()
        {
            IList<File> result = null;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }
            // 创建 Drive API 服务.
            using (Google.Apis.Drive.v3.DriveService service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            }))
            {
                try
                {
                    // 定义请求的参数.
                    FilesResource.ListRequest listRequest = service.Files.List();
                    listRequest.PageSize = 1000;
                    listRequest.Fields = "nextPageToken, files(id, name, size)";

                    // 列出文件.
                    result = listRequest.Execute().Files;
                }
                catch (Exception)
                {
                    return null;
                }
            }
            return result;
        }

3.1. 想云存储中写入数据

创建 FileCreate 函数用于向云存储中写一个文件。函数的输入参数是文件名和内容,它会以逻辑值返回运行的结果,如果文件成功创建还会返回云盘中文件的ID。已经熟悉的 Google.Apis.Drive.v3.DriveService 类用于创建文件,而 Google.Apis.Drive.v3.FilesResource.CreateMediaUpload 类则用于发送请求。在文件参数中,我们指出这将是一个简单的文本文件并给出可以复制的许可。

       public bool FileCreate(string name, string value, out string id)
        {
            bool result = false;
            id = null;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }
            using (var service = new Google.Apis.Drive.v3.DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            }))

            {
                var body = new File();
                body.Name = name;
                body.MimeType = "text/json";
                body.ViewersCanCopyContent = true;

                byte[] byteArray = Encoding.Default.GetBytes(value);
                using (var stream = new System.IO.MemoryStream(byteArray))
                {
                    Google.Apis.Drive.v3.FilesResource.CreateMediaUpload request = service.Files.Create(body, stream, body.MimeType);
                    if (request.Upload().Exception == null)
                    { id = request.ResponseBody.Id; result = true; }
                }
            }
            return result;
        }

创建文件之后的下一步是更新函数,让我们回顾一下我们应用程序的目标以及 Google Drive 文件系统的特性。我们正在开发用于在位于不同电脑上的多个终端之间交换数据的应用程序,我们不知道在什么时间会有多少终端这些信息,但是,云文件系统的特性允许我们创建多个相同文件名和扩展名的文件,这使我们可以首先使用新的数据创建一个新的文件,然后从云存储中删除旧数据的文件,这就是 FileUpdate 函数所做的事情。它的输入参数是文件名和它的内容,并且它会以逻辑值返回运行的结果。

在函数的开始,我们声明 new_id 文本变量并且调用之前创建的 FileCreate 函数,它会在云中创建一个新的数据文件并把新文件的ID返回给我们的变量,

然后我们使用 GetFileList 函数取得云存储中所有文件的列表,把它们挨个与新创建文件的名称和ID相比较,所有不需要的重复内容会从存储中删除。这里我们再次使用已经了解的 Google.Apis.Drive.v3.DriveService 类,而请求是使用 Google.Apis.Drive.v3.FilesResource.DeleteRequest 类来发送的。

        public bool FileUpdate(string name, string value)
        {
            bool result = false;
            if (credential == null)
                this.Authorize();
            if (credential == null)
            {
                return result;
            }

            string new_id;
            if (FileCreate(name, value, out new_id))
            {
                IList<File> files = GetFileList();
                if (files != null && files.Count > 0)
                {
                    result = true;
                    try
                    {
                        using (var service = new DriveService(new BaseClientService.Initializer()
                        {
                            HttpClientInitializer = credential,
                            ApplicationName = ApplicationName,
                        }))
                        {
                            foreach (var file in files)
                            {
                                if (file.Name == name && file.Id != new_id)
                                {
                                    try
                                    {
                                        Google.Apis.Drive.v3.FilesResource.DeleteRequest request = service.Files.Delete(file.Id);
                                        string res = request.Execute();
                                    }
                                    catch (Exception)
                                    {
                                        continue;
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception)
                    {
                        return result;
                    }
                }
            }
            return result;
        }

3.2. 从云存储中读取数据

我们已经创建函数用于向云存储中写入数据,现在是时候把它们读取回来了。我们记得,在下载文件之前,我们需要从云中取得它的ID,这就是 GetFileID 函数的目标。它的输入参数是所需文件的名称,而返回值是它的ID。函数的逻辑构建很简单:我们从 GetFileList 函数取得文件列表,然后通过文件排序找到第一个所需名称的文件,很可能它是最旧的文件,只是这一次有风险,可能会生成一个所需参数的新文件或者在下载的时候出错,让我们接受这些风险以取得完整的数据,在下一次更新中会下载最新的变化。我们记得,所有不需要的重复都在 FileUpdate 函数中创建新数据文件的时候删除了。

        public string GetFileId(string name)
        {
            string result = null;
            IList<File> files = GetFileList();

            if (files != null && files.Count > 0)
            {
                foreach (var file in files)
                {
                    if (file.Name == name)
                    {
                        result = file.Id;
                        break;
                    }
                }
            }
            return result;
        }

在取得了文件ID之后,我们就能从中取得我们所需的数据了。为此,我们需要 FileRead 函数, 传入所需的文件ID,而函数会返回它的内容。如果没有成功,函数会返回一个空的字符串。和以前一样,我们需要 Google.Apis.Drive.v3.DriveService 类来创建一个连接,然后使用 Google.Apis.Drive.v3.FilesResource.GetRequest 类来创建请求。

        public string FileRead(string id)
        {
            if (String.IsNullOrEmpty(id))
            {
                return ("错误. 没有找到文件");
            }
            bool result = false;
            string value = null;
            if (credential == null)
                this.Authorize();
            if (credential != null)
            {
                using (var service = new DriveService(new BaseClientService.Initializer()
                {
                    HttpClientInitializer = credential,
                    ApplicationName = ApplicationName,
                }))
                {
                    Google.Apis.Drive.v3.FilesResource.GetRequest request = service.Files.Get(id);
                    using (var stream = new MemoryStream())
                    {
                        request.MediaDownloader.ProgressChanged += (IDownloadProgress progress) =>
                        {
                            if (progress.Status == DownloadStatus.Completed)
                                result = true;
                        };
                        request.Download(stream);

                        if (result)
                        {
                            int start = 0;
                            int count = (int)stream.Length;
                            value = Encoding.Default.GetString(stream.GetBuffer(), start, count);
                        }
                    }
                }
            }
            return value;
        }

3.3. 使用终端应用程序创建一些交互

现在我们已经把我们的应用程序和 Google Drive 云存储相连了,是时候也把它连接到 MetaTrader 应用程序了,毕竟,这是它的主要目标。我决定使用命名管道来建立这个连接,网站上已经有描述如何使用它们,并且 MQL5 语言已经提供了 CFilePipe 类用于操作这个连接类型,这将使我们在创建应用程序时工作更加简单。

终端允许运行多个应用程序,所以,我们的“桥梁”应该能够同时处理多个连接。让我们为此使用异步多线程编程模式。

我们应该定义在桥梁和应用程序之间传输消息的格式,为了从云中读取文件,我们应该传入命令和文件名称。为了把文件写到云中,我们应该发送命令,文件名和它的内容。因为在管道中数据的传输是单线程的,所以可以在一个字符串中传入整个信息,我使用了 ";" 作为字符串中的栏位分隔符。

首先,让我们声明全局变量:

  • Drive — 之前创建的用于操作云存储的类;
  • numThreads — 设置同时运行的线程数量;
  • pipeName — 用于保存我们管道名称的字符串变量;
  • servers — 运行线程的数组.
        GoogleDriveClass Drive = new GoogleDriveClass();
        private static int numThreads = 10;
        private static string pipeName = "GoogleBridge";
        static Thread[] servers;

创建用于运行线程的函数 PipesCreate。在这个函数中,我们初始化我们线程的数组并在循环中运行它们。当运行每个线程时,会调用 ServerThread 函数来初始化我们线程中的函数。

        public void PipesCreate()
        {
            int i;
            servers = new Thread[numThreads];

            for (i = 0; i < numThreads; i++)
            {
                servers[i] = new Thread(ServerThread);
                servers[i].Start();
            }
        }

另外,在每个线程开始的时候还会创建一个命名管道,并且运行等待客户端连接到管道的异步函数。当有客户端连接到管道时,会调用 Connected 函数。为了达到这个效果,我们创建 AsyncCallback asyn_connected delegate(委托). 如果有异常发生,线程会重新启动。

        private void ServerThread()
        {
            NamedPipeServerStream pipeServer =
                new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);

            int threadId = Thread.CurrentThread.ManagedThreadId;
            // 等待客户端连接
            AsyncCallback asyn_connected = new AsyncCallback(Connected);
            try
            {
                pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
            }
            catch (Exception)
            {
                servers[threadId].Suspend();
                servers[threadId].Start();
            }
        }

当客户端连接到命名管道时,我们检查管道的状态,如果有异常,就重新启动该线程。如果连接稳定,我们就启动从应用程序中读取请求的函数,如果读取函数返回 false, 就重新启动连接。

        private void Connected(IAsyncResult pipe)
        {
            if (!pipe.IsCompleted)
                return;
            bool exit = false;
            try
            {
                NamedPipeServerStream pipeServer = (NamedPipeServerStream)pipe.AsyncState;
                try
                {
                    if (!pipeServer.IsConnected)
                        pipeServer.WaitForConnection();
                }
                catch (IOException)
                {
                    AsyncCallback asyn_connected = new AsyncCallback(Connected);
                    pipeServer.Dispose();
                    pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, numThreads, PipeTransmissionMode.Message, PipeOptions.Asynchronous);
                    pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
                    return;
                }
                while (!exit && pipeServer.IsConnected)
                {
                    // 从客户端读取请求. 当客户端
                    // 写入管道时,它的安全令牌就可用了.

                    while (pipeServer.IsConnected)
                    {
                        if (!ReadMessage(pipeServer))
                        {
                            exit = true;
                            break;
                        }
                    }
                    //等待一个客户端的连接
                    AsyncCallback asyn_connected = new AsyncCallback(Connected);
                    pipeServer.Disconnect();
                    pipeServer.BeginWaitForConnection(asyn_connected, pipeServer);
                    break;
                }
            }
            finally
            {
                exit = true;
            }
        }

ReadMessage 函数读取和处理来自应用程序的请求。一个线程对象的引用被传入函数作为参数,函数的运行结果是逻辑值。首先,该函数从命名管道读取应用程序的请求并把它分成栏位,然后它会识别命令并进行需要的操作,

函数支持三个命令:

  • Close — 关闭当前的连接;
  • Read — 从云存储读取文件;
  • Write — 把文件写到云存储.

为了关闭当前连接,函数只要简单返回 false,调用它的 Connected 函数会处理剩余的事情。

为了执行文件读取请求,我们应当定义文件ID并使用上面描述过的 GetFileID 和 FileRead 函数来读取它的内容。

在执行文件写入函数之后,要调用之前创建的 FileUpdate 函数。

当然,不要忘记处理异常,如果有异常,要再次登录 Google。

        private bool ReadMessage(PipeStream pipe)
        {
            if (!pipe.IsConnected)
                return false;

            byte[] arr_read = new byte[1024];
            string message = null;
            int length;
            do
            {
                length = pipe.Read(arr_read, 0, 1024);
                if (length > 0)
                    message += Encoding.Default.GetString(arr_read, 0, length);
            } while (length >= 1024 && pipe.IsConnected);
            if (message == null)
                return true;

            if (message.Trim() == "Close\0")
                return false;

            string result = null;
            string[] separates = { ";" };
            string[] arr_message = message.Split(separates, StringSplitOptions.RemoveEmptyEntries);
            if (arr_message[0].Trim() == "Read")
            {
                try
                {
                    result = Drive.FileRead(Drive.GetFileId(arr_message[1].Trim() + GoogleDriveClass.extension));
                }
                catch (Exception e)
                {
                    result = "错误 " + e.ToString();
                    Drive.Authorize();
                }
                return WriteMessage(pipe, result);
            }

            if (arr_message[0].Trim() == "Write")
            {
                try
                {
                    result = (Drive.FileUpdate(arr_message[1].Trim() + GoogleDriveClass.extension, arr_message[2].Trim()) ?"完成" : "错误");
                }
                catch (Exception e)
                {
                    result = "错误 " + e.ToString();
                    Drive.Authorize();
                }

                return WriteMessage(pipe, result);
            }
            return true;
        }

在处理请求之后,我们应当把运行结果返回给应用程序,让我们创建 WriteMessage 函数。它的参数是当前命名管道对象的引用和一条要发送给应用程序的消息,该函数会以逻辑值返回运行结果。

        private bool WriteMessage(PipeStream pipe, string message)
        {
            if (!pipe.IsConnected)
                return false;
            if (message == null || message.Count() == 0)
                message = "Empty";
            byte[] arr_bytes = Encoding.Default.GetBytes(message);
            try
            {
                pipe.Flush();
                pipe.Write(arr_bytes, 0, arr_bytes.Count());
                pipe.Flush();
            }
            catch (IOException)
            {
                return false;
            }
            return true;
        }

现在我们已经描述了所有所需的函数,是时候运行 PipesCreate 函数了。我创建了 Windows Form 项目,所以我是从 Form1 函数中运行这个函数的。

        public Form1()
        {
            InitializeComponent();
            PipesCreate();
        }

我们现在要做的就是重新编译项目,并把包含云存储访问数据的 json 文件复制到应用程序文件夹。   

4. 创建 MetaTrader 应用程序

让我们转到我们程序的实际应用部分,首先,我建议您写一个简单程序用来复制简单的图形对象。

4.1. 用于操作图形对象的类

我们应该传递什么样的对象数据来在另外的图表上重新创建它呢?也许,应该是对象类型和用来标识它的名称,我们将还需要对象颜色和它的坐标。第一个问题是我们应该传递多少坐标以及它们的数值是什么,例如,当传递垂直线的数据时,传递一个时间就足够了,当处理水平线的时候,我们应当传入一个价格。对于趋势线,我们需要两对坐标 - 时间,价格和线的方向(右边/左边)。不同的对象有通用和独特的参数。但是,在 MQL5 中, 所有的对象都是使用四个函数来创建和修改的: ObjectCreate, ObjectSetInteger, ObjectSetDouble 和 ObjectSetString。我们将遵照相同的方法,传递参数类型,属性和数值。

让我们创建参数类型的枚举。

enum ENUM_SET_TYPE
  {
   ENUM_SET_TYPE_INTEGER=0,
   ENUM_SET_TYPE_DOUBLE=1,
   ENUM_SET_TYPE_STRING=2
  };

创建 CCopyObject 类用于处理对象数据。在初始化时传入一个字符串型参数,随后,它识别我们类在图表上创建的对象,我们将把这个值保存到 s_ObjectsID 类变量。

class CCopyObject
  {
private:
   string            s_ObjectsID;

public:
                     CCopyObject(string objectsID="CopyObjects");
                    ~CCopyObject();
  };
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
CCopyObject::CCopyObject(string objectsID="CopyObjects")
  {
   s_ObjectsID = (objectsID==NULL || objectsID=="" ? "CopyObjects" : objectsID);
  }

4.1.1. 用于收集对象数据的函数

创建 CreateMessage 函数。它的参数是所需图表的ID。该函数返回一个文本值,用于发送到云存储,包含了对象参数和值的列表。返回的字符串应该是结构化的,这样数据才能被读取。让我们统一确定每个对象的数据都放到花括号中,"|" 符号会被用于参数之间的分隔符,而 "=" 符号分隔参数和它的值。在每个对象描述的开始,要指出它的名称和类型,并且随后会调用对应它的类型的对象描述函数。

string CCopyObject::CreateMessage(long chart)
  {
   string result = NULL;
   int total = ObjectsTotal(chart, 0);
   for(int i=0;i<total;i++)
     {
      string name = ObjectName(chart, i, 0);
      switch((ENUM_OBJECT)ObjectGetInteger(chart,name,OBJPROP_TYPE))
        {
         case OBJ_HLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_HLINE)+"|"+HLineToString(chart, name)+"}";
           break;
         case OBJ_VLINE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_VLINE)+"|"+VLineToString(chart, name)+"}";
           break;
         case OBJ_TREND:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_TREND)+"|"+TrendToString(chart, name)+"}";
           break;
         case OBJ_RECTANGLE:
           result+="{NAME="+name+"|TYPE="+IntegerToString(OBJ_RECTANGLE)+"|"+RectangleToString(chart, name)+"}";
           break;
        }
     }
   return result;
  }

例如,HLineToString 函数被调用以描述一条水平线,图表 ID 和对象名称用作它的参数。函数会返回结构化的字符串,包含了对象的参数。例如,对于水平线,传递的参数为价格,颜色,线宽以及线是在图表前方还是背景上显示的。不要忘记,在参数属性之前要从之前创建的枚举中选择设置参数类型。

string CCopyObject::HLineToString(long chart,string name)
  {
   string result = NULL;
   if(ObjectFind(chart,name)!=0)
      return result;
   
   result+=IntegerToString(ENUM_SET_TYPE_DOUBLE)+"="+IntegerToString(OBJPROP_PRICE)+"=0="+DoubleToString(ObjectGetDouble(chart,name,OBJPROP_PRICE,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_COLOR)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_COLOR,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_STYLE)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_STYLE,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_BACK)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_BACK,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_INTEGER)+"="+IntegerToString(OBJPROP_WIDTH)+"=0="+IntegerToString(ObjectGetInteger(chart,name,OBJPROP_WIDTH,0))+"|";
   result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TEXT)+"=0="+ObjectGetString(chart,name,OBJPROP_TEXT,0)+"|";
   result+=IntegerToString(ENUM_SET_TYPE_STRING)+"="+IntegerToString(OBJPROP_TOOLTIP)+"=0="+ObjectGetString(chart,name,OBJPROP_TOOLTIP,0);
   return result;
  }

类似地,要创建函数用来描述其他的对象类型,在我的例子中,有 VLineToString 来描述垂直线,TrendToString 用于趋势线而 RectangleToString 用于长方形。这些函数的代码可以在附件中的类代码中找到。

4.1.2. 用于在图表上绘制对象的函数

我们已经创建了用于数据收集的函数,现在,让我们开发函数用于读取消息并在图表上绘制对象: DrawObjects. 它的参数是图表 ID 和接收的消息。该函数返回操作执行结果的逻辑值。

函数的算法包含几个步骤:

  • 把字符串消息根据对象分成字符串数组;

  • 把每个对象数组元素分成参数的数组;

  • 在参数数组中寻找名称和对象类型. 把我们的ID加到名称中;

  • 根据获得的名称在图表上寻找对象;如果对象不在主窗口或者它的类型与消息中指定的不同,就删除它;

  • 如果还没有对象或者在前一步中被删除,就在图表上创建一个新的对象;

  • 把消息中接收到的对象属性传输到我们图表上的对象中(使用另外的 CopySettingsToObject 函数);

  • 从图表上删除不需要的对象 (由另外的 DeleteExtraObjects 函数进行)。 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值