Fabric实战(10)链码(chaincode)开发-shim包API
本文章所有操作基于的操作系统版本是:ubuntu16.04 64位
本文章基于的Fabric网络环境是《Fabric实战(2)运行一个简单的fabric网络(容器外)》
1 golang版本的chaincode的代码
1.1 chaincode源代码结构
下面看一最简单的chaincode代码以及相关的解释:
//包名
//一个chaincode通常是一个golang源码文件,这个包名必须是main
package main//导入包
//chaincode需要引入一些Fabric提供的系统包,这些系统包提供了chaincode和Fabirc进行通信的接口。
import ("fmt""strconv""github.com/hyperledger/fabric/core/chaincode/shim"pb "github.com/hyperledger/fabric/protos/peer"
)//定义chaincode主结构体
//每个chaincode都需要定义个结构体,结构体的名字可以是任意符合golang命名规范的字符串。chaincode的住结构体必须实现Chaincode接口
// type Chaincode interface {// Init(stub ChaincodeStubInterface) pb.Response
// Invoke(stub ChaincodeStubInterface) pb.Response
// }
type SimpleChaincode struct {}//Init方法
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {return shim.Success(nil)
}func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {function, args := stub.GetFunctionAndParameters()if function == "invoke" {return t.invoke(stub, args)}return shim.Error("Invalid invoke function name. Expecting \"invoke\"")
}func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response {return shim.Success(nil)
}//main函数
//调用shim包的Start方法,启动chaincode,如果启动成功,这个函数会一直阻塞在这个地方,不会退出。
func main() {err := shim.Start(new(SimpleChaincode))if err != nil {fmt.Printf("Error starting Simple chaincode: %s", err)}
}
1.1.1 Init方法
Init方法是chaincode的初始化方法,当执行peer chaincode instantiate实例化chaincode的时候会调用该方法,同时-c 参数后面内容会作为参数传入Init方法中,以下面的实例化命令为例:
peer chaincode instantiate -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["init", "a", "100", "b", "200"]}' -P "OR ('Org1MSP.member', 'Org2MSP.member')"
上面的命令给chaincode传入4个参数"a", “100”, “b” ,“200”。注意命令中Args后面一共有五个参数,其中第一个参数init是固定值,后面才是参数。在Init函数中可以通过下面的方法获取参数:
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {//获取客户端传入的参数,args是一个字符串切片,存储传入的字符串参数_, args := stub.GetFunctionAndParameters()return shim.Success(nil)
}
1.1.2 Invoke方法
Invoke方法的主要作用是写入数据。在执行命令peer chaincode invoke的时候会调用这个方法,同时会把命令中-c后面的参数传入Invoke方法中。以下面的invoke命令为例:
peer chaincode invoke -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["invoke", "a", "b", "1"]}'
上面的命令调用chaincode的Invoke方法并传入三个参数, 注意Args后面数组中的第一个值"invoke"是默认的固定参数。
1.2 shim包的方法
shim包常用方法
Success #向客户端返回正确消息
Error #向客户端返回错误消息
Start #启动chaincode
LogLevel #将字符串类型转化成LoggingLevel类型
SetLoggingLevel #设置链码shim包运行日志等级
IsEnabledForLogLevel #检查合约的是否使能制定的等级
1.2.1 Success方法
Success方法负责将正确的消息返回给调用chaincode的客户端。
//定义
func Success(payload []byte) pb.Response //示例
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {return shim.Success([]byte("sucess invoke"))
}
1.2.2 Error方法
Error方法负责将错误的信息返回给调用Chaincode的客户端。
// 定义
func Error(msg string) pb.Response //示例
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {return shim.Error([]byte("fail invoke"))
}
1.2.3 SetLoggingLevel方法
设置合约的shim包运行日志等级
// 定义
func SetLoggingLevel(level LoggingLevel) //示例
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {//将"debug"字符串转换成LoggingLevel类型logleve, _ := shim.LogLevel("DEBUG")//设置日志等级为debugshim.SetLoggingLevel(logleve)return shim.Success([]byte("sucess invoke"))
}
日志等级如下:
"CRITICAL" 0
"ERROR" 1
"WARNING" 2
"NOTICE" 3
"INFO" 4
"DEBUG" 5
1.2.4 IsEnabledForLogLevel方法
检查合约是否使能了日志等级
//定义
func IsEnabledForLogLevel(logLevel string) bool //示例
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {enabled := shim.IsEnabledForLogLevel("DEBUG")if enabled{fmt.Println("Contract enabled debug level")}return shim.Success([]byte("sucess invoke"))
}
注意:只要IsEnabledForLogLevel的参数小于当前的等级,都是返回true。比如当前的等级是DEBUG,IsEnabledForLogLevel传入的参数是ERROR,返回值也是true
1.3 shim包ChaincodeStubInterface接口
Shim包中有一个接口ChaincodeStubInterface,在Invoke方法和Query方法方法中该接口作为参数传入方法中。ChaincodeStubInterface接口提供了一组方法,通过该组方法可以非常方便的操作账本数据。ChaincodeStubInterface接口的核心方法大概可以分为四大类:
- 系统管理
- 存储管理
- 交易管理
- 调用外部chaincode
方法名列表:
GetFunctionAndParameters #获取调用者传入的参数
GetArgs #获取调用者传入参数,返回二维字节切片
GetStringArgs #获取调用者传入参数,返回一维字符串切片
GetArgsSlice #获取调用者传入参数,返回字节切片
PutState #存储数据到账本
GetState #从账本中获取指定的数据
DelState #删除账本中的数据
GetStateByRange #查询指定key指定范围的数据
CreateCompositeKey #创建复合键
GetStatePyPartialCompositeKey #通过复合键取值
SplitCompositeKey #拆分复合键
GetHistoryForKey #获取制定key的历史记录
GetTxID #获取交易的编号
GetTxTimestamp #获取交易的时间
GetCreator #获取交易的创建者
InvokeChaincode #调用其他的chaincode
1.3.1 系统管理接口
1.3.1.1 GetFunctionAndParameters
该方法负责接收调用者传过来的参数。
//方法定义
GetFunctionAndParameters() (string, []string)//调用
peer chaincode instantiate -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["init", "a", "100", "b", "200"]}' -P "OR ('Org1MSP.member', 'Org2MSP.member')"//chaincode
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {fn, args := stub.GetFunctionAndParameters()fmt.Println(fn, args)return shim.Success(nil)
}//输出
init [a 100 b 200]
1.3.1.2 GetArgs
获取调用者传入参数,返回二维字节切片
//方法定义
GetArgs() [][]byte//调用
peer chaincode instantiate -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["init", "a", "100", "b", "200"]}' -P "OR ('Org1MSP.member', 'Org2MSP.member')"//chaincode
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {args := stub.GetArgs()fmt.Println(args)return shim.Success(nil)
}//输出
[[105 110 105 116] [97] [49 48 48] [98] [50 48 48]]
1.3.1.3 GetStringArgs
获取调用者传入参数,返回一维字符串切片
//方法定义
GetStringArgs() []string//调用
peer chaincode instantiate -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["init", "a", "100", "b", "200"]}' -P "OR ('Org1MSP.member', 'Org2MSP.member')"//chaincode
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {args := stub.GetStringArgs()fmt.Println(args)return shim.Success(nil)
}//输出
[init a 100 b 200]
1.3.1.4 GetArgsSlice
获取调用者传入参数,返回字节切片
//方法定义
GetArgsSlice() ([]byte, error)//调用
peer chaincode instantiate -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["init", "a", "100", "b", "200"]}' -P "OR ('Org1MSP.member', 'Org2MSP.member')"//chaincode
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {args, err := stub.GetArgsSlice()if err != nil{fmt.Println(err)}fmt.Println(args )return shim.Success(nil)
}//输出
[105 110 105 116 97 49 48 48 98 50 48 48]
1.3.2 存储管理接口
1.3.2.1 PutState
该方法负责把数据保存到fabric账本中。
//方法定义
PutState(key string, value []byte) error//chaincode代码示例
type Student struct {Name stringAge uint8Score uint8
}func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {stu := Student{"zhangsan",25,99,}bytes, err := json.Marshal(stu)if err != nil {return shim.Error(err.Error())}stub.PutState("zhangsan", bytes)return shim.Success(nil)
}
1.3.2.2 GetState
该方法负责从fabric账本中读取数据
//方法定义
GetState(key string) ([]byte, error)//chaincode代码示例
type Student struct {Name stringAge uint8Score uint8
}func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {bytes, err := stub.GetState("zhangsan")if err != nil{return shim.Error(err.Error())}stu := new(Student)err = json.Unmarshal(bytes, stu)if err != nil{return shim.Error(err.Error())}fmt.Println(stu)return shim.Success(nil)
}
1.3.2.3 DelState
DelState用来删除账本中的key,如果对一个不存在的的key-value进行删除不会返回错误
//方法定义
DelState(key string) error
//chaincode代码示例
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {err := stub.DelState("zhangsan")if err != nil{fmt.Println(err)}return shim.Success(nil)
}
1.3.2.4 GetStateByRange
根据key的范围来查询数据
//方法定义
//包含startKey,不包含endKey
//如果startKey和endKey为空,则无限范围查询
GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)//chaincode代码示例
func (t *SimpleChaincode) set(stub shim.ChaincodeStubInterface, args []string)pb.Response {stub.PutState("1", []byte("zhangsan"))stub.PutState("2", []byte("lisi"))stub.PutState("3", []byte("wangwu"))stub.PutState("4", []byte("zhaoliu"))stub.PutState("5", []byte("sunqi"))return shim.Success(nil)
}func (t *SimpleChaincode) get(stub shim.ChaincodeStubInterface, args []string) pb.Response {keysIter, err := stub.GetStateByRange("2", "5")if err != nil{return shim.Error(err.Error())}rsp := make(map[string]string)for keysIter.HasNext(){response, interErr := keysIter.Next()if interErr != nil{return shim.Error(interErr.Error())}rsp[response.Key] = string(response.Value)fmt.Println(response.Key, string(response.Value))}//将结果以json字符串返回jsonRsp, err := json.Marshal(rsp)if err != nil{return shim.Error(err.Error())}return shim.Success(jsonRsp)
}func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {function, args := stub.GetFunctionAndParameters()if function == "set" {return t.set(stub, args)} else if function == "get" {return t.get(stub, args)}return shim.Success(nil)
}
链码日志输出:
2 lisi
3 wangwu
4 zhaoliu
1.3.2.5 GetHistoryForKey
查询某个键的历史修改记录。返回的记录包括交易的编号、修改的值、当前key的有没有被删除,交易发生的时间戳,GetHistoryForKey需要peer的配置参数core.ledger.history.enableHistoryDatabase为true。
注意:
1.查询交易所在区块中其他更新操作,本次查询无法查询到。
//方法定义
GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)
//chaincode代码示例
func (t *SimpleChaincode) set(stub shim.ChaincodeStubInterface, args []string)pb.Response {name := args[0]err := stub.PutState("userName", []byte(name))if err != nil{return shim.Error(err.Error())}return shim.Success(nil)
}func (t *SimpleChaincode) history(stub shim.ChaincodeStubInterface, args []string)pb.Response {keyInter, err := stub.GetHistoryForKey("name")if err != nil{return shim.Error(err.Error())}for keyInter.HasNext(){response, interErr := keyInter.Next()if interErr != nil{return shim.Error(interErr.Error())}txid := response.TxIdtxvalue := response.Valuetxstatus := response.IsDeletetxtimestamp := response.Timestamptm := time.Unix(txtimestamp.Seconds, 0)timeString := tm.Format("2006-01-02 03:04:05 PM")fmt.Println(txid, string(txvalue), txstatus, timeString)}return shim.Success(nil)
}
链码日志输出:
先调用链码的set方法修改三次,然后在调用history方法获取记录
peer chaincode invoke -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["set", "zhangsan"]}'peer chaincode invoke -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["set", "lisi"]}'peer chaincode invoke -o orderer.simple-network.com:7050 -C testchannel -n r_test_cc6 -v 1.0 -c '{"Args":["set", "wangwu"]}'
bdfd3d29622d41350493d5421d6759c56a38273033a31e3d70e091856e62dc69 zhangsan false 2019-03-14 03:28:02 PM
692dba2bf1c7c24ede17248310df58726a974210afe2097b3ff10ce8a3381ab5 lisi false 2019-03-14 03:28:02 PM
5864c768fad78049969891a315838ed1336fa86ed138f2ca87aee4e09900b4bc wangwu false 2019-03-14 03:28:02 PM
1.3.2.4 CreateCompositeKey
创建组合间,用户查询
//方法定义
CreateCompositeKey(objectType string, attributes []string) (string, error)//chaincode代码示例
func (t *SimpleChaincode) set(stub shim.ChaincodeStubInterface, args []string)pb.Response {stus := []Student{{"lisi", 20, 99},{"lisi",20,98},{"lisi",21,100},}for i, _ := range(stus) {stu := stus[i]key, err := stub.CreateCompositeKey(stu.Name, []string{strconv.Itoa(stu.Age), strconv.Itoa(stu.Score)})if err != nil {fmt.Println(err)return shim.Error(err.Error())}bytes, err := json.Marshal(stu)if err != nil {fmt.Println(err)return shim.Error(err.Error())}stub.PutState(key, bytes)}return shim.Success(nil)
}func (t *SimpleChaincode) get(stub shim.ChaincodeStubInterface, args []string) pb.Response {rs, err := stub.GetStateByPartialCompositeKey("lisi", []string{})if err != nil{fmt.Println(err)return shim.Error(err.Error())}defer rs.Close()for rs.HasNext(){responseRange, err := rs.Next()if err != nil{fmt.Println(err)}stu := new(Student)err = json.Unmarshal(responseRange.Value, stu)if err != nil{fmt.Println(err)}fmt.Println(responseRange.Key, stu)}return shim.Success(nil)
}
链码日志输出:
lisi2098 &{lisi 20 98}
lisi2099 &{lisi 20 99}
lisi21100 &{lisi 21 100}
由于stub.GetStateByPartialCompositeKey(“lisi”, []string{}),只指定了一个主键,所以把叫lisi的学生都查询出来了。
如果按修改代码如下:
rs, err := stub.GetStateByPartialCompositeKey("lisi", []string{"20"})
链码日志输出如下:
lisi2098 &{lisi 20 98}
lisi2099 &{lisi 20 99}
由于21岁的lisi不满足组合键要求,所以没有查询出来
1.3.2.5 GetStatePyPartialCompositeKey
通过组合键进行查询, 相当于模糊查询
//方法定义
GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)//chaincode代码示例
调用示例参考CreateCompositeKey方法的示例
1.3.2.6 SplitCompositeKey
分割组合键, 将创建的组合键分割开来
//方法定义
SplitCompositeKey(compositeKey string) (string, []string, error)
//chaincode代码示例
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {key, _ := stub.CreateCompositeKey("sili", []string{"20", "100"})key1, keyPart, _:= stub.SplitCompositeKey(key)fmt.Println(key1, keyPart)return shim.Success(nil)
}
链码日志输出:
sili20100
sili [20 100]
1.3.3 交易管理接口
1.3.3.1 GetTxID
获取当前调用交易的ID号
//方法定义
GetTxID() string//chaincode代码示例
func (t *SimpleChaincode) get(stub shim.ChaincodeStubInterface, args []string) pb.Response {id := stub.GetTxID()fmt.Println(id)return shim.Success(nil)
}
链码日志输出:
66a925816afb2c2067f014a29ad8609ae40a405d5997f79c4c930e4d79c2eb5f
1.3.3.2 GetCreator
获得提案的签名者证书
//方法定义
GetTxID() string//chaincode代码示例
func (t *SimpleChaincode) get(stub shim.ChaincodeStubInterface, args []string) pb.Response {id := stub.GetTxID()fmt.Println(id)return shim.Success(nil)
}
链码日志输出:
Org1MSP�-----BEGIN -----
MIICLjCCAdWgAwIBAgIQJcxoHNiOAWmIXH7VZ/6fDDAKBggqhkjOPQQDAjCBgTEL
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
cmFuY2lzY28xIDAeBgNVBAoTF29yZzEuc2ltcGxlLW5ldHdvcmsuY29tMSMwIQYD
VQQDExpjYS5vcmcxLnNpbXBsZS1uZXR3b3JrLmNvbTAeFw0xOTAzMTAxNTA5MzBa
Fw0yOTAzMDcxNTA5MzBaMGIxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9y
bmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMSYwJAYDVQQDDB1BZG1pbkBvcmcx
LnNpbXBsZS1uZXR3b3JrLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJWM
1LNOMnDGnbpe0XEx7HNFyBZgOZk5E+PDtHer/ZbbFvCIvoFoafzw7qIwlOxXTj99
bOG3kHQX2OPp/+7/nEajTTBLMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAA
MCsGA1UdIwQkMCKAIPXBTNkkfW7yIcRyZvip+yXmvzwPBujCNja1BMv3f+BmMAoG
CCqGSM49BAMCA0cAMEQCIG1Q+qvhCSv/GZtINP51WZLUpUDtABiY8RsmetPqK1Rh
AiBsYoFWSQeVRspiTuEz1dhW+ke2iq05P3k+gFzN1yiUUw==
-----END -----
1.3.3.3 GetTxTimestamp
//方法定义
GetTxTimestamp() (*timestamp.Timestamp, error)//chaincode代码示例
func (t *SimpleChaincode) get(stub shim.ChaincodeStubInterface, args []string) pb.Response {timestamp, _ := stub.GetTxTimestamp()tm:= time.Unix(timestamp.Seconds, 0)ts := tm.Format("2006-01-02 03:04:05 PM")fmt.Println(ts)return shim.Success(nil)
}
链码日志输出:
2019-03-14 06:12:49 PM
1.3.4 调用外部chaincode
1.3.4.1 InvokeChaincode
在chaincode中可以调用其他的chaincode。
//方法定义
InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response//chaincode代码示例
func (t *SimpleChaincode) set(stub shim.ChaincodeStubInterface, args []string)pb.Response {parms := []string{"get"}querArgs := make([][]byte, len(parms))for i, arg := range parms{querArgs[i] = []byte(arg)}response := stub.InvokeChaincode("r_test_cc8", querArgs, "testchannel")if response.Status != shim.OK{err := fmt.Sprintln("fail to query chaincode, Got error :%s", response.Payload)shim.Error(err)}return shim.Success(nil)
}//r_test_cc8链码中的get函数实现
func (t *SimpleChaincode) get(stub shim.ChaincodeStubInterface, args []string) pb.Response {fmt.Println("Thank you for calling me")return shim.Success(nil)
}
r_test_cc8链码日志输出:
Thank you for calling me
Fabric实战(10)链码(chaincode)开发-shim包API相关推荐
- Hyperledger Fabric 实战(七):链码 shim API 详解
为什么80%的码农都做不了架构师?>>> 用 Go 的链码的 shim API 主要方法详解 GetFunctionAndParameters 获取方法名和参数 invoke的 ...
- Fabric学习笔记(六) - chaincode开发模式
启动网络 terminal1 cd /opt/gopath/src/github.com/hyperledger/fabric-samples/chaincode-docker-devmodedock ...
- Fabric - chaincode开发模式
Fabric学习笔记(六) - chaincode开发模式 启动网络 terminal1 cd /opt/gopath/src/github.com/hyperledger/fabric-sample ...
- Hyperledger Fabric 1.0 实战开发系列 第三课 chaincode开发
chaincode是由go语言写的,实现了定义的接口.其他语言例如JAVA也是支持的.通过application体积的transaction,chaincode可以初始化并管理Ledger状态. 一个 ...
- Hyperledger Fabric Chaincode 开发
好了,进入正题.我今天分享的内容的题目是Fabric1.0 Chaincode介绍.除了介绍Chaincode程序编写.调试的基本方法之外,我还加入了一些有关Chaincode原理的内容,希望能够帮助 ...
- Hyperledger Fabric chaincode 开发(疑难解答)
Q&A Q1: 使用fabric release 1.2 进行golang chaincode开发时报错: ..\..\hyperledger\fabric\vendor\github.com ...
- hyperledger fabric 实战开发——水产品溯源交易平台(二)
文章目录 前言 一.技术学习 1.Hyperledger fabric 1.1 流程 1.2 配置 1.3 范例解析并自写 1.3 算法实现 二.Web编写 前言 hyperledger fabric ...
- Fabric 链码Chaincode 的安装、初始化、调用、升级
Fabric 链码Chaincode 的安装.初始化.调用.升级 Fabric chaincode 上一篇文章,我们启动了一个Fabric网络,这篇文章来看看在Fabric网络进行应用的开发. 上一篇 ...
- Fabric chaincode开发调试
Fabric chaincode开发调试 由于chaincode开发调试步骤稍多,每次都要查看官方doc有些不便,且偶尔还会遇到官方doc无法访问的情况,故整理一份chaincode开发步骤(环境已经 ...
- Fabric系列 - 链码 ChainCode
链码(chaincode)是 Hyperledger Fabric 提供的智能合约,是上层应用与底层区块链平台交互的媒介.现阶段,Fabric 提供 Go.Java.Node.js 等语言编写的链码 ...
最新文章
- nodejs 开发企业微信第三方应用入门教程
- java 复杂报表_Java+POI+模板”一:打造复杂Excel 报表
- 北大青鸟广州天河:高中生做技术经理!
- 原始尺寸_螺母尺寸检测,螺丝螺母外观检测设备
- win8.1 计算机放在桌面,Win8.1怎么把开始屏幕中的程序放到桌面?
- SAP Spartacus UsersSelectors.getAddressesLoading
- 使用.NET 5自动查找代码中的潜在错误
- 都 2021 了,你还忘记关闭 http body?
- 关于3Q大战和反垄断
- 光标移动事件。 gridview光标移动变色
- R语言生存分析之竞争风险模型
- android手机锁屏密码忘记,安卓手机锁屏密码忘了怎么办 解决锁屏密码六种方法介绍...
- 徐思201771010132《面向对象程序设计(java)》第十六周学习总结
- 第二人生的源码分析(三十九)关闭WinXP的错误报告功能
- Linux系统 运行小花仙游戏(针对2021年Flash停止维护的情况)
- 最佳二次逼近多项式MATLAB代码,数学实验“Chebshev多项式最佳一致逼近,最佳平方逼近”实验报告(内含matlab程序).doc...
- 重建古老计算机Pong
- python计算平均分_自动计算平均学分绩点的Python实现
- 使用C#实现邮箱验证
- JavaScript之BOM